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/02/28 19:42:38 UTC
(groovy) 01/01: GROOVY-9381: Support async/await like ES7
This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch GROOVY-9381_2
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 86c39238448bc988764706847cb4be57897fd8dc
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Mar 1 04:35:02 2026 +0900
GROOVY-9381: Support async/await like ES7
---
build.gradle | 2 +
gradle/verification-metadata.xml | 16 +
src/antlr/GroovyLexer.g4 | 2 +
src/antlr/GroovyParser.g4 | 14 +-
src/main/java/groovy/concurrent/AsyncStream.java | 67 +
.../groovy/concurrent/AsyncStreamGenerator.java | 117 ++
src/main/java/groovy/concurrent/AwaitResult.java | 101 +
src/main/java/groovy/concurrent/Awaitable.java | 122 ++
.../java/groovy/concurrent/AwaitableAdapter.java | 76 +
.../concurrent/AwaitableAdapterRegistry.java | 181 ++
src/main/java/groovy/concurrent/GroovyPromise.java | 127 ++
src/main/java/groovy/transform/Async.java | 121 ++
src/main/java/groovy/transform/AsyncUtils.java | 524 +++++
.../apache/groovy/parser/antlr4/AstBuilder.java | 152 +-
.../java/org/codehaus/groovy/ast/ModifierNode.java | 2 +
.../groovy/transform/AsyncASTTransformation.java | 306 +++
.../groovy/transform/AsyncAwaitSyntaxTest.groovy | 2108 ++++++++++++++++++++
.../groovy/transform/AsyncClosureLambdaTest.groovy | 793 ++++++++
.../transform/AsyncExceptionHandlingTest.groovy | 1161 +++++++++++
.../transform/AsyncFrameworkIntegrationTest.groovy | 506 +++++
.../groovy/transform/AsyncTransformTest.groovy | 1401 +++++++++++++
.../groovy/transform/AsyncVirtualThreadTest.groovy | 1115 +++++++++++
versions.properties | 2 +
23 files changed, 9013 insertions(+), 3 deletions(-)
diff --git a/build.gradle b/build.gradle
index ed3f22feaa..5fe1efbfb1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -122,6 +122,8 @@ dependencies {
testImplementation "com.thoughtworks.qdox:qdox:${versions.qdox}"
testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
+ testImplementation "io.reactivex.rxjava3:rxjava:${versions.rxjava3}"
+ testImplementation "io.projectreactor:reactor-core:${versions.reactor}"
testFixturesImplementation projects.groovyXml
testFixturesImplementation projects.groovyTest
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 3ec4ebf779..340c7c52be 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -114,6 +114,7 @@
<ignored-key id="C71FB765CD9DE313" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="C7CA19B7B620D787" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="CA80D1F0EB6CA4BA" reason="Key couldn't be downloaded from any key server"/>
+ <ignored-key id="D1031D14464180E0" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="D2151178A123C97F" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="D364ABAA39A47320" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="D7742D58455ECC7C" reason="Key couldn't be downloaded from any key server"/>
@@ -851,6 +852,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"/>
@@ -2206,6 +2217,11 @@
<sha512 value="adcc480f68828ffd68d03846be852988b595c2e1bb69224d273578dd6c2ad2773edfe96625a7c00bc40ae0f2d1cac8412eaa54b88cc8e681b0b4c0ee3b082333" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="org.reactivestreams" name="reactive-streams" version="1.0.4">
+ <artifact name="reactive-streams-1.0.4.jar">
+ <sha512 value="cdab6bd156f39106cd6bbfd47df1f4b0a89dc4aa28c68c31ef12a463193c688897e415f01b8d7f0d487b0e6b5bd2f19044bf8605704b024f26d6aa1f4f9a2471" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
+ </artifact>
+ </component>
<component group="org.reflections" name="reflections" version="0.10.2">
<artifact name="reflections-0.10.2.jar">
<pgp value="3F2A008A91D11A7FAC4A0786F13D3E721D56BD54"/>
diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index 79ddf78dbd..9846b309bf 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -392,6 +392,8 @@ DEF : 'def';
IN : 'in';
TRAIT : 'trait';
THREADSAFE : 'threadsafe'; // reserved keyword
+ASYNC : 'async';
+AWAIT : 'await';
// §3.9 Keywords
BuiltInPrimitiveType
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 88e3b8b713..7becfb6030 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -134,6 +134,7 @@ modifier
| VOLATILE
| DEF
| VAR
+ | ASYNC
)
;
@@ -600,7 +601,7 @@ switchStatement
;
loopStatement
- : FOR LPAREN forControl RPAREN nls statement #forStmtAlt
+ : FOR AWAIT? LPAREN forControl RPAREN nls statement #forStmtAlt
| WHILE expressionInPar nls statement #whileStmtAlt
| DO nls statement nls WHILE expressionInPar #doWhileStmtAlt
;
@@ -642,6 +643,7 @@ statement
| continueStatement #continueStmtAlt
| { inSwitchExpressionLevel > 0 }?
yieldStatement #yieldStmtAlt
+ | YIELD RETURN nls expression #yieldReturnStmtAlt
| identifier COLON nls statement #labeledStmtAlt
| assertStatement #assertStmtAlt
| localVariableDeclaration #localVariableDeclarationStmtAlt
@@ -778,13 +780,17 @@ 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
+
// qualified names, array expressions, method invocation, post inc/dec
| postfixExpression #postfixExprAlt
| switchExpression #switchExprAlt
- // ~(BNOT)/!(LNOT) (level 1)
+ // ~(BNOT)/!(LNOT)/await (level 1)
| (BITNOT | NOT) nls expression #unaryNotExprAlt
+ | AWAIT nls expression #awaitExprAlt
// math power operator (**) (level 2)
| left=expression op=POWER nls right=expression #powerExprAlt
@@ -1228,6 +1234,8 @@ identifier
: Identifier
| CapitalizedIdentifier
| AS
+ | ASYNC
+ | AWAIT
| IN
| PERMITS
| RECORD
@@ -1246,6 +1254,8 @@ keywords
: ABSTRACT
| AS
| ASSERT
+ | ASYNC
+ | AWAIT
| BREAK
| CASE
| CATCH
diff --git a/src/main/java/groovy/concurrent/AsyncStream.java b/src/main/java/groovy/concurrent/AsyncStream.java
new file mode 100644
index 0000000000..6e83cc96d4
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncStream.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+/**
+ * Asynchronous iteration abstraction, analogous to C#'s
+ * {@code IAsyncEnumerable<T>} or JavaScript's async iterables.
+ * <p>
+ * Used with the {@code for await} syntax:
+ * <pre>
+ * for await (item in asyncStream) {
+ * process(item)
+ * }
+ * </pre>
+ * <p>
+ * Third-party reactive types (Reactor {@code Flux}, RxJava {@code Observable})
+ * can be adapted to {@code AsyncStream} via {@link AwaitableAdapter}.
+ *
+ * @param <T> the element type
+ * @see AwaitableAdapter
+ * @since 6.0.0
+ */
+public interface AsyncStream<T> {
+
+ /**
+ * Asynchronously advances to the next element. Returns an {@link Awaitable}
+ * that completes with {@code true} if an element is available, or
+ * {@code false} if the stream is exhausted.
+ */
+ Awaitable<Boolean> moveNext();
+
+ /**
+ * Returns the current element. Must only be called after {@link #moveNext()}
+ * has completed with {@code true}.
+ */
+ T getCurrent();
+
+ /**
+ * Returns an empty {@code AsyncStream} that completes immediately.
+ */
+ @SuppressWarnings("unchecked")
+ static <T> AsyncStream<T> empty() {
+ return (AsyncStream<T>) EMPTY;
+ }
+
+ /** Singleton empty stream instance. */
+ AsyncStream<Object> EMPTY = new AsyncStream<>() {
+ @Override public Awaitable<Boolean> moveNext() { return Awaitable.of(false); }
+ @Override public Object getCurrent() { return null; }
+ };
+}
diff --git a/src/main/java/groovy/concurrent/AsyncStreamGenerator.java b/src/main/java/groovy/concurrent/AsyncStreamGenerator.java
new file mode 100644
index 0000000000..17a19c975d
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncStreamGenerator.java
@@ -0,0 +1,117 @@
+/*
+ * 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.concurrent.SynchronousQueue;
+
+/**
+ * A producer/consumer implementation of {@link AsyncStream} used by
+ * {@code async} methods that contain {@code yield return} statements.
+ * <p>
+ * The producer (method body) runs on a separate thread and calls
+ * {@link #yield(Object)} for each emitted element. The consumer
+ * calls {@link #moveNext()}/{@link #getCurrent()} — typically via
+ * a {@code for await} loop.
+ * <p>
+ * Uses a {@link SynchronousQueue} to provide natural back-pressure:
+ * the producer thread blocks at each {@code yield return} until the
+ * consumer has consumed the previous element (mirroring C#'s async
+ * iterator suspension semantics).
+ *
+ * @param <T> the element type
+ * @since 6.0.0
+ */
+public class AsyncStreamGenerator<T> implements AsyncStream<T> {
+
+ private static final Object DONE = new Object();
+
+ private final SynchronousQueue<Object> queue = new SynchronousQueue<>();
+ private T current;
+
+ /**
+ * Produces the next element. Called from the generator body when
+ * a {@code yield return expr} statement is executed. Blocks until
+ * the consumer is ready.
+ */
+ public void yield(Object value) {
+ try {
+ queue.put(new Item(value));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new java.util.concurrent.CancellationException("Interrupted during yield");
+ }
+ }
+
+ /**
+ * Signals that the generator has completed (no more elements).
+ */
+ public void complete() {
+ try {
+ queue.put(DONE);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Signals that the generator failed with an exception.
+ */
+ public void error(Throwable t) {
+ try {
+ queue.put(new ErrorItem(t));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Awaitable<Boolean> moveNext() {
+ try {
+ Object next = queue.take();
+ if (next == DONE) {
+ return Awaitable.of(false);
+ }
+ if (next instanceof ErrorItem ei) {
+ Throwable cause = ei.error;
+ if (cause instanceof Error err) throw err;
+ throw sneakyThrow(cause);
+ }
+ current = (T) ((Item) next).value;
+ return Awaitable.of(true);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new java.util.concurrent.CancellationException("Interrupted during moveNext");
+ }
+ }
+
+ @Override
+ public T getCurrent() {
+ return current;
+ }
+
+ // Wrapper to handle null values in the queue
+ private record Item(Object value) { }
+ private record ErrorItem(Throwable error) { }
+
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
+ throw (T) t;
+ }
+}
diff --git a/src/main/java/groovy/concurrent/AwaitResult.java b/src/main/java/groovy/concurrent/AwaitResult.java
new file mode 100644
index 0000000000..52e77d2342
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitResult.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Represents the outcome of an asynchronous computation that may have
+ * succeeded or failed. This is used by {@code awaitAllSettled()} —
+ * the Groovy equivalent of JavaScript's {@code Promise.allSettled()}.
+ * <p>
+ * An {@code AwaitResult} is either a {@linkplain #isSuccess() success}
+ * carrying a value, or a {@linkplain #isFailure() failure} carrying a
+ * {@link Throwable}.
+ *
+ * @param <T> the value type
+ * @since 6.0.0
+ */
+public final class AwaitResult<T> {
+
+ private final T value;
+ private final Throwable error;
+ private final boolean success;
+
+ private AwaitResult(T value, Throwable error, boolean success) {
+ this.value = value;
+ this.error = error;
+ this.success = success;
+ }
+
+ /**
+ * Creates a successful result with the given value.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> AwaitResult<T> success(Object value) {
+ return new AwaitResult<>((T) value, null, true);
+ }
+
+ /**
+ * Creates a failure result with the given exception.
+ */
+ public static <T> AwaitResult<T> failure(Throwable error) {
+ return new AwaitResult<>(null, Objects.requireNonNull(error), false);
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public boolean isFailure() {
+ return !success;
+ }
+
+ /**
+ * Returns the value if successful; throws {@link IllegalStateException} if failed.
+ */
+ public T getValue() {
+ if (!success) throw new IllegalStateException("Cannot get value from a failed result");
+ return value;
+ }
+
+ /**
+ * Returns the exception if failed; throws {@link IllegalStateException} if successful.
+ */
+ public Throwable getError() {
+ if (success) throw new IllegalStateException("Cannot get error from a successful result");
+ return error;
+ }
+
+ /**
+ * Returns the value if successful, or applies the given function to
+ * the error to produce a fallback value.
+ */
+ public T getOrElse(Function<Throwable, ? extends T> fallback) {
+ return success ? value : fallback.apply(error);
+ }
+
+ @Override
+ public String toString() {
+ return success
+ ? "AwaitResult.Success[" + value + "]"
+ : "AwaitResult.Failure[" + error + "]";
+ }
+}
diff --git a/src/main/java/groovy/concurrent/Awaitable.java b/src/main/java/groovy/concurrent/Awaitable.java
new file mode 100644
index 0000000000..c429ae2cdd
--- /dev/null
+++ b/src/main/java/groovy/concurrent/Awaitable.java
@@ -0,0 +1,122 @@
+/*
+ * 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.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+
+/**
+ * Core abstraction for asynchronous computations in Groovy, decoupled from
+ * any specific JDK or third-party async API. Analogous to C#'s {@code Task<T>},
+ * JavaScript's {@code Promise<T>}.
+ * <p>
+ * {@code Awaitable} is the return type of {@code async} methods and the input
+ * type for the {@code await} expression. Third-party frameworks (RxJava, Reactor,
+ * etc.) can integrate by registering an {@link AwaitableAdapter} via
+ * {@link AwaitableAdapterRegistry}.
+ * <p>
+ * The default implementation, {@link GroovyPromise}, delegates to
+ * {@link CompletableFuture} but users never need to depend on that detail.
+ *
+ * @param <T> the result type
+ * @see GroovyPromise
+ * @see AwaitableAdapter
+ * @since 6.0.0
+ */
+public interface Awaitable<T> {
+
+ /**
+ * Blocks until the computation completes and returns the result.
+ */
+ T get() throws InterruptedException, ExecutionException;
+
+ /**
+ * Blocks until the computation completes or the timeout expires.
+ */
+ T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
+
+ /**
+ * Returns {@code true} if the computation has completed (normally or exceptionally).
+ */
+ boolean isDone();
+
+ /**
+ * Attempts to cancel the computation. If the computation has not yet started
+ * or is still running, it will be cancelled with a {@link CancellationException}.
+ * Analogous to C#'s {@code CancellationToken}.
+ *
+ * @return {@code true} if the computation was successfully cancelled
+ */
+ boolean cancel();
+
+ /**
+ * Returns {@code true} if the computation was cancelled before completing normally.
+ */
+ boolean isCancelled();
+
+ /**
+ * Returns {@code true} if this computation completed exceptionally
+ * (including cancellation).
+ */
+ boolean isCompletedExceptionally();
+
+ /**
+ * Returns a new {@code Awaitable} whose result is obtained by applying the
+ * given function to this awaitable's result when it completes.
+ */
+ <U> Awaitable<U> then(Function<? super T, ? extends U> fn);
+
+ /**
+ * Returns a new {@code Awaitable} produced by applying the given function
+ * to this awaitable's result, flattening the nested {@code Awaitable}.
+ */
+ <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn);
+
+ /**
+ * Returns a new {@code Awaitable} that, if this one completes exceptionally,
+ * applies the given function to the exception to produce a recovery value.
+ */
+ Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn);
+
+ /**
+ * Converts this {@code Awaitable} to a JDK {@link CompletableFuture}
+ * for interoperability with APIs that require it.
+ */
+ CompletableFuture<T> toCompletableFuture();
+
+ // ---- Static factories ----
+
+ /**
+ * Returns an already-completed {@code Awaitable} with the given value.
+ */
+ static <T> Awaitable<T> of(T value) {
+ return new GroovyPromise<>(CompletableFuture.completedFuture(value));
+ }
+
+ /**
+ * Returns an already-failed {@code Awaitable} with the given exception.
+ */
+ static <T> Awaitable<T> failed(Throwable error) {
+ return new GroovyPromise<>(CompletableFuture.failedFuture(error));
+ }
+}
diff --git a/src/main/java/groovy/concurrent/AwaitableAdapter.java b/src/main/java/groovy/concurrent/AwaitableAdapter.java
new file mode 100644
index 0000000000..5a1d52da54
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapter.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.concurrent;
+
+/**
+ * Service Provider Interface (SPI) for adapting third-party asynchronous types
+ * to Groovy's {@link Awaitable} and {@link AsyncStream} abstractions.
+ * <p>
+ * Implementations are discovered automatically via {@link java.util.ServiceLoader}.
+ * To register an adapter, create a file
+ * {@code META-INF/services/groovy.concurrent.AwaitableAdapter} containing the
+ * fully-qualified class name of your implementation.
+ * <p>
+ * <b>Example – RxJava adapter:</b>
+ * <pre>
+ * public class RxJavaAwaitableAdapter implements AwaitableAdapter {
+ * public boolean supportsAwaitable(Class<?> type) {
+ * return io.reactivex.rxjava3.core.Single.class.isAssignableFrom(type);
+ * }
+ * public <T> Awaitable<T> toAwaitable(Object source) {
+ * Single<T> single = (Single<T>) source;
+ * return new GroovyPromise<>(single.toCompletionStage().toCompletableFuture());
+ * }
+ * // ... supportsAsyncStream / toAsyncStream for Observable, Flowable, etc.
+ * }
+ * </pre>
+ *
+ * @see AwaitableAdapterRegistry
+ * @since 6.0.0
+ */
+public interface AwaitableAdapter {
+
+ /**
+ * Returns {@code true} if this adapter can convert instances of the given
+ * type to {@link Awaitable}.
+ */
+ boolean supportsAwaitable(Class<?> type);
+
+ /**
+ * Converts the given source object to an {@link Awaitable}.
+ * Called only when {@link #supportsAwaitable} returned {@code true}.
+ */
+ <T> Awaitable<T> toAwaitable(Object source);
+
+ /**
+ * Returns {@code true} if this adapter can convert instances of the given
+ * type to {@link AsyncStream}.
+ */
+ default boolean supportsAsyncStream(Class<?> type) {
+ return false;
+ }
+
+ /**
+ * Converts the given source object to an {@link AsyncStream}.
+ * Called only when {@link #supportsAsyncStream} returned {@code true}.
+ */
+ default <T> AsyncStream<T> toAsyncStream(Object source) {
+ throw new UnsupportedOperationException("AsyncStream conversion not supported by " + getClass().getName());
+ }
+}
diff --git a/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java b/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
new file mode 100644
index 0000000000..16968853b5
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
@@ -0,0 +1,181 @@
+/*
+ * 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.Iterator;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CopyOnWriteArrayList;
+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 for {@link CompletableFuture} and {@link Future} is always present
+ * as the lowest-priority fallback.
+ * <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<>();
+
+ 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.
+ */
+ public static void register(AwaitableAdapter adapter) {
+ ADAPTERS.add(0, adapter);
+ }
+
+ /**
+ * Converts the given source to an {@link Awaitable}.
+ * If the source is already an {@code Awaitable}, it is returned as-is.
+ *
+ * @throws IllegalArgumentException if no adapter supports the source type
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> toAwaitable(Object source) {
+ if (source instanceof Awaitable) return (Awaitable<T>) source;
+ Class<?> type = source.getClass();
+ for (AwaitableAdapter adapter : ADAPTERS) {
+ if (adapter.supportsAwaitable(type)) {
+ return adapter.toAwaitable(source);
+ }
+ }
+ throw new IllegalArgumentException(
+ "No AwaitableAdapter found for type: " + type.getName()
+ + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader.");
+ }
+
+ /**
+ * Converts the given source to an {@link AsyncStream}.
+ * If the source is already an {@code AsyncStream}, it is returned as-is.
+ *
+ * @throws IllegalArgumentException if no adapter supports the source type
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> AsyncStream<T> toAsyncStream(Object source) {
+ if (source instanceof AsyncStream) return (AsyncStream<T>) source;
+ Class<?> type = source.getClass();
+ for (AwaitableAdapter adapter : ADAPTERS) {
+ if (adapter.supportsAsyncStream(type)) {
+ return adapter.toAsyncStream(source);
+ }
+ }
+ throw new IllegalArgumentException(
+ "No AsyncStream adapter found for type: " + type.getName()
+ + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader.");
+ }
+
+ /**
+ * Built-in adapter handling JDK {@link CompletableFuture}, {@link CompletionStage},
+ * {@link Future}, 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 {
+ 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 (Exception e) {
+ cf.completeExceptionally(e.getCause() != null ? e.getCause() : e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/groovy/concurrent/GroovyPromise.java b/src/main/java/groovy/concurrent/GroovyPromise.java
new file mode 100644
index 0000000000..ba96003ef3
--- /dev/null
+++ b/src/main/java/groovy/concurrent/GroovyPromise.java
@@ -0,0 +1,127 @@
+/*
+ * 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.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.
+ *
+ * @param <T> the result type
+ * @see Awaitable
+ * @since 6.0.0
+ */
+public class GroovyPromise<T> implements Awaitable<T> {
+
+ private final CompletableFuture<T> future;
+
+ public GroovyPromise(CompletableFuture<T> future) {
+ this.future = Objects.requireNonNull(future, "future must not be null");
+ }
+
+ /**
+ * Creates a {@code GroovyPromise} wrapping the given {@link CompletableFuture}.
+ */
+ public static <T> GroovyPromise<T> of(CompletableFuture<T> future) {
+ return new GroovyPromise<>(future);
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ return future.get();
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ return future.get(timeout, unit);
+ }
+
+ @Override
+ public boolean isDone() {
+ return future.isDone();
+ }
+
+ @Override
+ public boolean cancel() {
+ return future.cancel(true);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return future.isCancelled();
+ }
+
+ @Override
+ public boolean isCompletedExceptionally() {
+ return future.isCompletedExceptionally();
+ }
+
+ @Override
+ public <U> Awaitable<U> then(Function<? super T, ? extends U> fn) {
+ return new GroovyPromise<>(future.thenApply(fn));
+ }
+
+ @Override
+ public <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn) {
+ return new GroovyPromise<>(future.thenCompose(t -> fn.apply(t).toCompletableFuture()));
+ }
+
+ @Override
+ public Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn) {
+ return new GroovyPromise<>(future.exceptionally(t -> {
+ // Unwrap all wrapper layers so handler sees the original exception
+ Throwable cause = t;
+ while (cause.getCause() != null
+ && (cause instanceof CompletionException
+ || cause instanceof ExecutionException
+ || cause instanceof java.lang.reflect.UndeclaredThrowableException
+ || cause instanceof java.lang.reflect.InvocationTargetException)) {
+ cause = cause.getCause();
+ }
+ return fn.apply(cause);
+ }));
+ }
+
+ @Override
+ public CompletableFuture<T> toCompletableFuture() {
+ return future;
+ }
+
+ @Override
+ public String toString() {
+ if (future.isDone()) {
+ if (future.isCompletedExceptionally()) {
+ return "GroovyPromise{failed}";
+ }
+ return "GroovyPromise{completed}";
+ }
+ return "GroovyPromise{pending}";
+ }
+}
diff --git a/src/main/java/groovy/transform/Async.java b/src/main/java/groovy/transform/Async.java
new file mode 100644
index 0000000000..f8b0d509b4
--- /dev/null
+++ b/src/main/java/groovy/transform/Async.java
@@ -0,0 +1,121 @@
+/*
+ * 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 a
+ * {@link java.util.concurrent.CompletableFuture CompletableFuture}.
+ * <p>
+ * When applied to a method, the {@code @Async} transformation will:
+ * <ul>
+ * <li>Change the method's return type to {@code CompletableFuture<T>} (where {@code T} is the original return type)</li>
+ * <li>Wrap the method body to execute asynchronously via {@code CompletableFuture.supplyAsync()}</li>
+ * <li>Transform any {@code await(future)} calls within the method to use {@link AsyncUtils#await}</li>
+ * </ul>
+ * <p>
+ * The {@code await(future)} pattern inside an {@code @Async} method blocks the async thread until the
+ * given future completes and returns the unwrapped result. This provides a sequential programming model
+ * over asynchronous operations, similar to JavaScript's {@code async/await} or C#'s {@code async/await}.
+ * <p>
+ * <em>Example usage:</em>
+ * <pre>
+ * import groovy.transform.Async
+ * import java.util.concurrent.CompletableFuture
+ *
+ * class DataService {
+ *
+ * {@code @}Async
+ * def fetchUser(long id) {
+ * def profile = await(fetchProfile(id))
+ * def orders = await(fetchOrders(id))
+ * return [profile: profile, orders: orders]
+ * }
+ *
+ * CompletableFuture<Map> fetchProfile(long id) {
+ * CompletableFuture.supplyAsync { [name: "User$id"] }
+ * }
+ *
+ * CompletableFuture<List> fetchOrders(long id) {
+ * CompletableFuture.supplyAsync { ["order1", "order2"] }
+ * }
+ * }
+ *
+ * def service = new DataService()
+ * def future = service.fetchUser(1) // returns CompletableFuture
+ * def result = future.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 {@link AsyncUtils#async}:
+ * <pre>
+ * import static groovy.transform.AsyncUtils.*
+ *
+ * def future = async {
+ * def data = await(fetchFromRemote())
+ * return process(data)
+ * }
+ * def result = future.get()
+ * </pre>
+ * <p>
+ * <b>Custom Executor:</b> By default, tasks run on the {@code ForkJoinPool.commonPool()}.
+ * You can specify a custom executor by providing the name of a field in the class:
+ * <pre>
+ * class MyService {
+ * private final Executor myExecutor = Executors.newFixedThreadPool(10)
+ *
+ * {@code @}Async(executor = "myExecutor")
+ * def fetchData() {
+ * return await(remoteCall())
+ * }
+ * }
+ * </pre>
+ * <p>
+ * <b>Restrictions:</b>
+ * <ul>
+ * <li>Cannot be applied to abstract methods</li>
+ * <li>Cannot be applied to constructors</li>
+ * <li>Cannot be applied to methods that already return {@code CompletableFuture}</li>
+ * </ul>
+ *
+ * @see AsyncUtils
+ * @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 {@link java.util.concurrent.ForkJoinPool#commonPool()}.
+ *
+ * @return the executor field name, or empty string for the default executor
+ */
+ String executor() default "";
+}
diff --git a/src/main/java/groovy/transform/AsyncUtils.java b/src/main/java/groovy/transform/AsyncUtils.java
new file mode 100644
index 0000000000..45845f9b09
--- /dev/null
+++ b/src/main/java/groovy/transform/AsyncUtils.java
@@ -0,0 +1,524 @@
+/*
+ * 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 groovy.concurrent.AsyncStream;
+import groovy.concurrent.AsyncStreamGenerator;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.AwaitableAdapterRegistry;
+import groovy.concurrent.AwaitResult;
+import groovy.concurrent.GroovyPromise;
+import groovy.lang.Closure;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.Future;
+
+/**
+ * Runtime support for the {@code async}/{@code await} language feature and the
+ * {@link Async} annotation.
+ * <p>
+ * All overloads of {@code await()} go through the {@link AwaitableAdapterRegistry}
+ * so that third-party async types (RxJava {@code Single}, Reactor {@code Mono},
+ * etc.) are supported transparently once an adapter is registered.
+ * <p>
+ * Inside {@code async} methods, the {@code await expr} expression and
+ * {@code await(future)} calls are compiled to {@code AsyncUtils.await(expr)}.
+ * <p>
+ * Standalone usage:
+ * <pre>
+ * import static groovy.transform.AsyncUtils.*
+ *
+ * def future = async {
+ * def result = await someAsyncCall()
+ * return result * 2
+ * }
+ * </pre>
+ *
+ * @see Async
+ * @see Awaitable
+ * @see AwaitableAdapterRegistry
+ * @since 6.0.0
+ */
+public class AsyncUtils {
+
+ /**
+ * {@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;
+
+ 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;
+ }
+
+ private static volatile Executor defaultExecutor =
+ VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : ForkJoinPool.commonPool();
+
+ private AsyncUtils() { }
+
+ // ---- await overloads ------------------------------------------------
+
+ /**
+ * Awaits the result of an {@link Awaitable}.
+ * <p>
+ * Exception handling follows the same principle as C# and
+ * JavaScript: the <em>original</em> exception is rethrown transparently,
+ * even if it is a checked exception. Wrapper layers
+ * ({@link ExecutionException}, {@link CompletionException}) are fully
+ * stripped so that {@code try/catch} blocks around {@code await} work
+ * with the actual exception type.
+ */
+ public static <T> T await(Awaitable<T> awaitable) {
+ try {
+ return awaitable.get();
+ } catch (ExecutionException e) {
+ throw rethrowUnwrapped(e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new CancellationException("Interrupted while awaiting");
+ }
+ }
+
+ /**
+ * Awaits the result of a {@link CompletableFuture}.
+ */
+ 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}.
+ */
+ public static <T> T await(CompletionStage<T> stage) {
+ return await(stage.toCompletableFuture());
+ }
+
+ /**
+ * Awaits the result of a {@link Future}.
+ */
+ 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();
+ throw new CancellationException("Interrupted while awaiting future");
+ }
+ }
+
+ /**
+ * Awaits an arbitrary object by adapting it to {@link Awaitable} via the
+ * {@link AwaitableAdapterRegistry}. This is the fallback overload called
+ * by the {@code await} expression when the type is not statically known.
+ */
+ @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);
+ return await(AwaitableAdapterRegistry.<T>toAwaitable(source));
+ }
+
+ // ---- async ----------------------------------------------------------
+
+ /**
+ * Executes the given closure asynchronously on the specified executor,
+ * returning an {@link Awaitable}. This is the <b>unified entry point</b>
+ * used by both the AST transformation ({@code @Async} methods) and the
+ * runtime ({@code async} closures/lambdas).
+ * <p>
+ * Exceptions thrown inside the closure are wrapped in
+ * {@link CompletionException} (via {@link #wrapForFuture}) so that the
+ * {@code await} methods can unwrap them transparently.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> executeAsync(Closure<T> closure, Executor executor) {
+ return GroovyPromise.of(CompletableFuture.supplyAsync(() -> {
+ try {
+ return closure.call();
+ } catch (Throwable t) {
+ throw wrapForFuture(t);
+ }
+ }, executor));
+ }
+
+ /**
+ * Void variant of {@link #executeAsync(Closure, Executor)} for
+ * {@code @Async void} methods.
+ */
+ public static Awaitable<Void> executeAsyncVoid(Closure<?> closure, Executor executor) {
+ return GroovyPromise.of(CompletableFuture.runAsync(() -> {
+ try {
+ closure.call();
+ } catch (Throwable t) {
+ throw wrapForFuture(t);
+ }
+ }, executor));
+ }
+
+ /**
+ * Executes the given closure asynchronously using the default executor,
+ * returning an {@link Awaitable}.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Awaitable<T> async(Closure<T> closure) {
+ return executeAsync(closure, defaultExecutor);
+ }
+
+ /**
+ * Wraps a closure so that calling it executes the body asynchronously
+ * and returns an {@link Awaitable}. This is used by the {@code async}
+ * keyword on closures/lambdas that have parameters:
+ * <pre>
+ * def asyncAdd = async { x, y -> x + y }
+ * def result = await asyncAdd(1, 2) // Awaitable<Integer>
+ * </pre>
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Closure<Awaitable<T>> wrapAsync(Closure<T> closure) {
+ return new Closure<Awaitable<T>>(closure.getOwner(), closure.getThisObject()) {
+ @SuppressWarnings("unused")
+ public Awaitable<T> doCall(Object... args) {
+ return GroovyPromise.of(CompletableFuture.supplyAsync(() -> {
+ try {
+ return closure.call(args);
+ } catch (Throwable t) {
+ throw wrapForFuture(t);
+ }
+ }, defaultExecutor));
+ }
+ };
+ }
+
+ /**
+ * Wraps a generator closure (containing {@code yield return}) so that
+ * calling it returns an {@link groovy.concurrent.AsyncStream}. This is
+ * used by the {@code async} keyword on closures/lambdas with parameters
+ * that contain yield return statements:
+ * <pre>
+ * def range = async { n -> for (i in 1..n) { yield return i } }
+ * for await (item in range(5)) { println item }
+ * </pre>
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Closure<AsyncStream<T>> wrapAsyncGenerator(Closure<?> closure) {
+ return new Closure<AsyncStream<T>>(closure.getOwner(), closure.getThisObject()) {
+ @SuppressWarnings("unused")
+ public AsyncStream<T> doCall(Object... args) {
+ AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+ CompletableFuture.runAsync(() -> {
+ try {
+ Object[] allArgs = new Object[args.length + 1];
+ allArgs[0] = gen;
+ System.arraycopy(args, 0, allArgs, 1, args.length);
+ closure.call(allArgs);
+ gen.complete();
+ } catch (Throwable t) {
+ gen.error(t);
+ }
+ }, defaultExecutor);
+ return gen;
+ }
+ };
+ }
+
+ // ---- awaitAll / awaitAny / awaitAllSettled ---------------------------
+
+ /**
+ * Waits for all given awaitables and returns their results as a list.
+ * Analogous to JavaScript's {@code Promise.all()}.
+ * <p>
+ * If any awaitable fails, the <em>first</em> encountered exception is
+ * rethrown transparently (unwrapped).
+ */
+ @SuppressWarnings("unchecked")
+ public static List<Object> awaitAll(Object... awaitables) {
+ CompletableFuture<?>[] futures = new CompletableFuture[awaitables.length];
+ for (int i = 0; i < awaitables.length; i++) {
+ futures[i] = toCompletableFuture(awaitables[i]);
+ }
+ try {
+ CompletableFuture.allOf(futures).join();
+ } catch (CompletionException e) {
+ throw rethrowUnwrapped(e);
+ }
+ List<Object> results = new ArrayList<>(futures.length);
+ for (CompletableFuture<?> f : futures) {
+ results.add(f.join());
+ }
+ return results;
+ }
+
+ /**
+ * Waits for any one of the given awaitables and returns its result.
+ * Analogous to JavaScript's {@code Promise.race()}.
+ */
+ @SuppressWarnings("unchecked")
+ public static Object awaitAny(Object... awaitables) {
+ CompletableFuture<?>[] futures = new CompletableFuture[awaitables.length];
+ for (int i = 0; i < awaitables.length; i++) {
+ futures[i] = toCompletableFuture(awaitables[i]);
+ }
+ try {
+ return CompletableFuture.anyOf(futures).join();
+ } catch (CompletionException e) {
+ throw rethrowUnwrapped(e);
+ }
+ }
+
+ /**
+ * Waits for all given awaitables and returns a list of {@link AwaitResult}
+ * objects, each recording either a success value or a failure exception.
+ * Analogous to JavaScript's {@code Promise.allSettled()}.
+ * <p>
+ * Unlike {@link #awaitAll}, this method never throws — all failures are
+ * captured in the returned list so the caller can handle them individually.
+ */
+ @SuppressWarnings("unchecked")
+ public static List<AwaitResult<Object>> awaitAllSettled(Object... awaitables) {
+ CompletableFuture<?>[] futures = new CompletableFuture[awaitables.length];
+ for (int i = 0; i < awaitables.length; i++) {
+ futures[i] = toCompletableFuture(awaitables[i]);
+ }
+ // Wait for all to settle (allSettled = allOf on handle'd futures)
+ CompletableFuture.allOf(
+ java.util.Arrays.stream(futures)
+ .map(f -> f.handle((v, t) -> null))
+ .toArray(CompletableFuture[]::new)
+ ).join();
+
+ 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;
+ }
+
+ private static CompletableFuture<?> toCompletableFuture(Object source) {
+ if (source instanceof CompletableFuture<?> cf) return cf;
+ if (source instanceof Awaitable<?> a) return a.toCompletableFuture();
+ if (source instanceof CompletionStage<?> cs) return cs.toCompletableFuture();
+ return AwaitableAdapterRegistry.toAwaitable(source).toCompletableFuture();
+ }
+
+ // ---- for-await support ----------------------------------------------
+
+ /**
+ * Converts an arbitrary source to an {@link AsyncStream} via the adapter
+ * registry. Used by the {@code for await} statement's generated code.
+ * <p>
+ * Returns an empty stream for {@code null} input, following Groovy's
+ * null-safe iteration convention.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> AsyncStream<T> toAsyncStream(Object source) {
+ if (source == null) return AsyncStream.empty();
+ if (source instanceof AsyncStream) return (AsyncStream<T>) source;
+ return AwaitableAdapterRegistry.toAsyncStream(source);
+ }
+
+ // ---- yield return / async generator ----------------------------------
+
+ /**
+ * Safety overload: if {@code yield return} is used outside an async generator
+ * context (where the compiler hasn't injected {@code $asyncGen}), this 1-arg
+ * overload will be called, producing a clear error message.
+ */
+ public static void yieldReturn(Object value) {
+ throw new IllegalStateException(
+ "yield return can only be used inside an async generator method or closure");
+ }
+
+ /**
+ * Called by the generated code for {@code yield return expr} statements.
+ * Delivers the value to the consumer via the given
+ * {@link AsyncStreamGenerator}.
+ * <p>
+ * The generator is passed explicitly as a synthetic {@code $asyncGen}
+ * parameter injected into the generator closure by the compiler — no
+ * {@link ThreadLocal} is needed.
+ *
+ * @param generator the {@link AsyncStreamGenerator} instance
+ * @param value the value to yield
+ * @throws IllegalStateException if generator is null (programming error)
+ */
+ 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
+ * receives the {@link AsyncStreamGenerator} as its first argument and
+ * may use {@code yield return} statements to produce elements.
+ * <p>
+ * This method is called by the AST transformation for {@code async}
+ * methods and closures that contain {@code yield return} statements.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> AsyncStream<T> generateAsyncStream(Closure<?> body) {
+ AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+ CompletableFuture.runAsync(() -> {
+ try {
+ body.call(gen);
+ gen.complete();
+ } catch (Throwable t) {
+ gen.error(t);
+ }
+ }, defaultExecutor);
+ return gen;
+ }
+
+ // ---- executor configuration -----------------------------------------
+
+ /**
+ * Returns {@code true} if the running JVM supports virtual threads (JDK 21+).
+ */
+ public static boolean isVirtualThreadsAvailable() {
+ return VIRTUAL_THREADS_AVAILABLE;
+ }
+
+ public static Executor getExecutor() {
+ return defaultExecutor;
+ }
+
+ public static void setExecutor(Executor executor) {
+ defaultExecutor = executor != null
+ ? executor
+ : (VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : ForkJoinPool.commonPool());
+ }
+
+ // ---- internal -------------------------------------------------------
+
+ /**
+ * Deeply unwraps nested exception wrapper layers to find the original
+ * user exception. Handles:
+ * <ul>
+ * <li>{@link CompletionException} — from {@code CompletableFuture.join()}</li>
+ * <li>{@link ExecutionException} — from {@code Future.get()}</li>
+ * <li>{@link UndeclaredThrowableException} — from Groovy's closure-to-SAM
+ * proxy when the closure throws a checked exception</li>
+ * <li>{@link InvocationTargetException} — from reflection-based invocations</li>
+ * </ul>
+ * This handles arbitrary nesting depth that can occur when async operations
+ * are composed through multiple framework layers.
+ */
+ static Throwable deepUnwrap(Throwable t) {
+ Throwable cause = t;
+ while (cause.getCause() != null
+ && (cause instanceof CompletionException
+ || cause instanceof ExecutionException
+ || cause instanceof UndeclaredThrowableException
+ || cause instanceof InvocationTargetException)) {
+ cause = cause.getCause();
+ }
+ return cause;
+ }
+
+ /**
+ * Unwraps and rethrows the original exception transparently using the
+ * "sneaky throw" technique. This is the key to exception transparency:
+ * checked exceptions are rethrown without wrapping, exactly as C#
+ * and JavaScript handle {@code await} failures.
+ * <p>
+ * {@code try { await expr } catch (IOException e) { ... }} works correctly
+ * because the original {@code IOException} is thrown as-is, not wrapped in
+ * {@code RuntimeException}.
+ */
+ private static RuntimeException rethrowUnwrapped(Throwable wrapper) {
+ Throwable cause = deepUnwrap(wrapper);
+ if (cause instanceof Error err) throw err;
+ throw sneakyThrow(cause);
+ }
+
+ /**
+ * Rethrows a checked exception without wrapping it, using an unchecked
+ * cast trick. The JVM does not actually enforce checked-exception
+ * declarations at the bytecode level — this is a compile-time-only
+ * constraint. This technique is well-established (used by Lombok
+ * runtime, etc.) and is safe.
+ */
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ /**
+ * Wraps a throwable for storage in a {@link CompletableFuture}. Unlike
+ * the old approach of wrapping checked exceptions in {@code RuntimeException},
+ * this uses {@link CompletionException} which is the expected wrapper type
+ * for {@code CompletableFuture} and is always unwrapped by our
+ * {@code await} methods.
+ */
+ private static CompletionException wrapForFuture(Throwable t) {
+ if (t instanceof CompletionException ce) return ce;
+ return new CompletionException(t);
+ }
+}
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index ff07bc7fb1..6ff4cca025 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -97,6 +97,7 @@ import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -465,7 +466,12 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
@Override
- public ForStatement visitForStmtAlt(final ForStmtAltContext ctx) {
+ public Statement visitForStmtAlt(final ForStmtAltContext ctx) {
+ // 'for await' async iteration
+ if (ctx.AWAIT() != null) {
+ return visitForAwait(ctx);
+ }
+
Function<Statement, ForStatement> maker = this.visitForControl(ctx.forControl());
Statement loopBody = this.unpackStatement((Statement) this.visit(ctx.statement()));
@@ -473,6 +479,39 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return configureAST(maker.apply(loopBody), ctx);
}
+ private Statement visitForAwait(final ForStmtAltContext ctx) {
+ ForControlContext forCtrl = ctx.forControl();
+ EnhancedForControlContext enhCtrl = forCtrl.enhancedForControl();
+ if (enhCtrl == null) {
+ throw createParsingFailedException("for await requires enhanced for syntax: for await (item in source)", ctx);
+ }
+
+ ClassNode varType = this.visitType(enhCtrl.type());
+ String varName = this.visitIdentifier(enhCtrl.identifier());
+ Expression source = (Expression) this.visit(enhCtrl.expression());
+ Statement loopBody = this.unpackStatement((Statement) this.visit(ctx.statement()));
+
+ ClassNode asyncUtilsType = ClassHelper.make("groovy.transform.AsyncUtils");
+
+ // def $asyncStream = AsyncUtils.toAsyncStream(source)
+ Expression toStreamCall = callX(asyncUtilsType, "toAsyncStream", new ArgumentListExpression(source));
+ ExpressionStatement streamDecl = new ExpressionStatement(declX(varX("$asyncStream"), toStreamCall));
+
+ // while (AsyncUtils.await($asyncStream.moveNext()))
+ Expression moveNextCall = callX(varX("$asyncStream"), "moveNext");
+ Expression awaitCall = callX(asyncUtilsType, "await", new ArgumentListExpression(moveNextCall));
+ BooleanExpression condition = new BooleanExpression(awaitCall);
+
+ // def <varName> = $asyncStream.getCurrent()
+ Expression getCurrentCall = callX(varX("$asyncStream"), "getCurrent");
+ ExpressionStatement getItemStmt = new ExpressionStatement(declX(varX(varName, varType), getCurrentCall));
+
+ BlockStatement whileBody = block(getItemStmt, loopBody);
+ WhileStatement whileStmt = new WhileStatement(condition, whileBody);
+
+ return configureAST(block(streamDecl, whileStmt), ctx);
+ }
+
@Override
public Function<Statement, ForStatement> visitForControl(final ForControlContext ctx) {
// e.g. `for (var e : x) { }` and `for (e in x) { }`
@@ -865,6 +904,15 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return configureAST(this.visitYieldStatement(ctx.yieldStatement()), ctx);
}
+ @Override
+ public ExpressionStatement visitYieldReturnStmtAlt(final YieldReturnStmtAltContext ctx) {
+ Expression expr = (Expression) this.visit(ctx.expression());
+ Expression yieldCall = callX(
+ ClassHelper.make("groovy.transform.AsyncUtils"), "yieldReturn",
+ new ArgumentListExpression(expr));
+ return configureAST(new ExpressionStatement(yieldCall), ctx);
+ }
+
@Override
public ContinueStatement visitContinueStatement(final ContinueStatementContext ctx) {
if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
@@ -1699,6 +1747,12 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
} else { // script method declaration
methodNode = createScriptMethodNode(modifierManager, methodName, returnType, parameters, exceptions, code);
}
+
+ // Inject @Async annotation for methods declared with the 'async' keyword modifier
+ if (modifierManager.containsAny(ASYNC)) {
+ methodNode.addAnnotation(new AnnotationNode(ClassHelper.make("groovy.transform.Async")));
+ }
+
anonymousInnerClassList.forEach(e -> e.setEnclosingMethod(methodNode));
methodNode.setGenericsTypes(this.visitTypeParameters(ctx.typeParameters()));
@@ -2877,6 +2931,102 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
throw createParsingFailedException("Unsupported unary expression: " + ctx.getText(), ctx);
}
+ @Override
+ public Expression visitAwaitExprAlt(final AwaitExprAltContext ctx) {
+ Expression expr = (Expression) this.visit(ctx.expression());
+ return configureAST(
+ callX(ClassHelper.make("groovy.transform.AsyncUtils"), "await",
+ new ArgumentListExpression(expr)),
+ ctx);
+ }
+
+ @Override
+ public Expression visitAsyncClosureExprAlt(final AsyncClosureExprAltContext ctx) {
+ ClosureExpression closure = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
+ boolean hasUserParams = closure.getParameters() != null && closure.getParameters().length > 0;
+ boolean hasYieldReturn = containsYieldReturnCalls(closure);
+
+ ClassNode asyncUtils = ClassHelper.make("groovy.transform.AsyncUtils");
+ if (hasYieldReturn) {
+ // Inject synthetic $asyncGen as first parameter — rebuild closure
+ Parameter genParam = new Parameter(ClassHelper.DYNAMIC_TYPE, "$asyncGen");
+ 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)
+ injectGenParamIntoYieldReturnCalls(genClosure.getCode(), genParam);
+ String method = hasUserParams ? "wrapAsyncGenerator" : "generateAsyncStream";
+ return configureAST(callX(asyncUtils, method, new ArgumentListExpression(genClosure)), ctx);
+ } else {
+ String method = hasUserParams ? "wrapAsync" : "async";
+ return configureAST(callX(asyncUtils, method, new ArgumentListExpression(closure)), ctx);
+ }
+ }
+
+ /**
+ * Walks statement tree and transforms {@code AsyncUtils.yieldReturn(expr)} calls to
+ * {@code AsyncUtils.yieldReturn($asyncGen, expr)}, injecting the generator parameter
+ * as the first argument. Does not descend into nested closures.
+ */
+ private static void injectGenParamIntoYieldReturnCalls(Statement code, Parameter genParam) {
+ code.visit(new org.codehaus.groovy.ast.CodeVisitorSupport() {
+ @Override
+ public void visitExpressionStatement(org.codehaus.groovy.ast.stmt.ExpressionStatement stmt) {
+ Expression expr = stmt.getExpression();
+ if (expr instanceof org.codehaus.groovy.ast.expr.StaticMethodCallExpression smce
+ && "yieldReturn".equals(smce.getMethod())
+ && "groovy.transform.AsyncUtils".equals(smce.getOwnerType().getName())) {
+ VariableExpression genRef = varX("$asyncGen");
+ genRef.setAccessedVariable(genParam);
+ ArgumentListExpression newArgs = new ArgumentListExpression();
+ newArgs.addExpression(genRef);
+ if (smce.getArguments() instanceof ArgumentListExpression argList) {
+ for (Expression arg : argList.getExpressions()) {
+ newArgs.addExpression(arg);
+ }
+ }
+ org.codehaus.groovy.ast.expr.StaticMethodCallExpression replacement =
+ new org.codehaus.groovy.ast.expr.StaticMethodCallExpression(
+ smce.getOwnerType(), "yieldReturn", newArgs);
+ replacement.setSourcePosition(smce);
+ stmt.setExpression(replacement);
+ }
+ super.visitExpressionStatement(stmt);
+ }
+ @Override
+ public void visitClosureExpression(ClosureExpression expression) {
+ // Don't descend into nested closures
+ }
+ });
+ }
+
+ /**
+ * Checks whether a closure's AST body contains {@code AsyncUtils.yieldReturn()} calls,
+ * used to determine if an async closure is a generator.
+ */
+ private static boolean containsYieldReturnCalls(ClosureExpression closure) {
+ boolean[] found = {false};
+ closure.getCode().visit(new CodeVisitorSupport() {
+ @Override
+ public void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
+ if ("yieldReturn".equals(call.getMethod())) {
+ found[0] = true;
+ }
+ super.visitStaticMethodCallExpression(call);
+ }
+ });
+ return found[0];
+ }
+
@Override
public CastExpression visitCastExprAlt(final CastExprAltContext ctx) {
Expression expr = (Expression) this.visit(ctx.expression());
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..89c4bad4e1
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java
@@ -0,0 +1,306 @@
+/*
+ * 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 groovy.transform.AsyncUtils;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
+import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.GenericsType;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.VariableScope;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics;
+
+/**
+ * Handles code generation for the {@link Async} annotation and the {@code async}
+ * keyword modifier.
+ * <p>
+ * Transforms the annotated method so that:
+ * <ol>
+ * <li>{@code await(expr)} calls are redirected to {@link AsyncUtils#await}</li>
+ * <li>The method body is wrapped in {@code GroovyPromise.of(CompletableFuture.supplyAsync(...))}
+ * (or {@code runAsync} for void methods)</li>
+ * <li>The return type becomes {@link Awaitable}{@code <T>}</li>
+ * </ol>
+ *
+ * @since 6.0.0
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class AsyncASTTransformation extends AbstractASTTransformation {
+
+ private static final Class<?> MY_CLASS = Async.class;
+ private static final ClassNode MY_TYPE = ClassHelper.make(MY_CLASS);
+ private static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
+ private static final ClassNode AWAITABLE_TYPE = ClassHelper.make(Awaitable.class);
+ private static final ClassNode ASYNC_STREAM_TYPE = ClassHelper.make(AsyncStream.class);
+ private static final ClassNode ASYNC_UTILS_TYPE = ClassHelper.make(AsyncUtils.class);
+
+ @Override
+ public void visit(ASTNode[] nodes, SourceUnit source) {
+ init(nodes, source);
+ AnnotatedNode parent = (AnnotatedNode) nodes[1];
+ AnnotationNode anno = (AnnotationNode) nodes[0];
+ if (!MY_TYPE.equals(anno.getClassNode())) return;
+
+ if (!(parent instanceof MethodNode mNode)) return;
+
+ // Validate
+ if (mNode.isAbstract()) {
+ addError(MY_TYPE_NAME + " cannot be applied to abstract method '" + mNode.getName() + "'", mNode);
+ return;
+ }
+ if ("<init>".equals(mNode.getName()) || "<clinit>".equals(mNode.getName())) {
+ addError(MY_TYPE_NAME + " cannot be applied to constructors", mNode);
+ return;
+ }
+ ClassNode originalReturnType = mNode.getReturnType();
+ if (AWAITABLE_TYPE.getName().equals(originalReturnType.getName())
+ || ASYNC_STREAM_TYPE.getName().equals(originalReturnType.getName())
+ || "java.util.concurrent.CompletableFuture".equals(originalReturnType.getName())) {
+ addError(MY_TYPE_NAME + " cannot be applied to a method that already returns an async type", mNode);
+ return;
+ }
+ Statement originalBody = mNode.getCode();
+ if (originalBody == null) return;
+
+ ClassNode cNode = mNode.getDeclaringClass();
+
+ // Resolve executor expression
+ String executorFieldName = getMemberStringValue(anno, "executor");
+ Expression executorExpr;
+ if (executorFieldName != null && !executorFieldName.isEmpty()) {
+ FieldNode field = cNode.getDeclaredField(executorFieldName);
+ if (field == null) {
+ addError(MY_TYPE_NAME + ": executor field '" + executorFieldName
+ + "' not found in class " + cNode.getName(), mNode);
+ return;
+ }
+ if (mNode.isStatic() && !field.isStatic()) {
+ addError(MY_TYPE_NAME + ": executor field '" + executorFieldName
+ + "' must be static for static method '" + mNode.getName() + "'", mNode);
+ return;
+ }
+ executorExpr = varX(executorFieldName);
+ } else {
+ executorExpr = callX(ASYNC_UTILS_TYPE, "getExecutor", new org.codehaus.groovy.ast.expr.ArgumentListExpression());
+ }
+
+ // Step 1: Transform await() method calls to AsyncUtils.await()
+ new AwaitCallTransformer(source).visitMethod(mNode);
+
+ // Step 2: Check if body contains yield return (AsyncUtils.yieldReturn) calls
+ Statement transformedBody = mNode.getCode();
+ boolean hasYieldReturn = containsYieldReturn(transformedBody, source);
+
+ Parameter[] closureParams;
+ if (hasYieldReturn) {
+ // Generator: inject $asyncGen as closure parameter so yield return
+ // can reference it directly — no ThreadLocal needed
+ closureParams = new Parameter[]{ new Parameter(ClassHelper.DYNAMIC_TYPE, "$asyncGen") };
+ } 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
+ injectGenParamIntoYieldReturnCalls(transformedBody, closureParams[0]);
+
+ // Async generator: wrap body in AsyncUtils.generateAsyncStream { ... }
+ Expression genCall = callX(ASYNC_UTILS_TYPE, "generateAsyncStream", args(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 AsyncUtils.executeAsync/executeAsyncVoid
+ // This ensures identical exception handling with async closures/lambdas
+ boolean isVoid = ClassHelper.isPrimitiveVoid(originalReturnType);
+ Expression asyncCall;
+ if (isVoid) {
+ asyncCall = callX(ASYNC_UTILS_TYPE, "executeAsyncVoid", args(closure, executorExpr));
+ } else {
+ asyncCall = callX(ASYNC_UTILS_TYPE, "executeAsync", args(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 statement tree and transforms {@code AsyncUtils.yieldReturn(expr)} calls to
+ * {@code AsyncUtils.yieldReturn($asyncGen, expr)}, injecting the generator parameter
+ * as the first argument. Does not descend into nested closures.
+ */
+ private 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
+ && "yieldReturn".equals(smce.getMethod())
+ && "groovy.transform.AsyncUtils".equals(smce.getOwnerType().getName())) {
+ VariableExpression genRef = varX("$asyncGen");
+ 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(), "yieldReturn", newArgs);
+ replacement.setSourcePosition(smce);
+ stmt.setExpression(replacement);
+ }
+ super.visitExpressionStatement(stmt);
+ }
+ @Override
+ public void visitClosureExpression(ClosureExpression expression) {
+ // Don't descend into nested closures
+ }
+ });
+ }
+
+ /**
+ * Checks whether the given statement tree contains any
+ * {@code AsyncUtils.yieldReturn()} calls, indicating this is
+ * an async generator method (returns {@link AsyncStream}).
+ */
+ private static boolean containsYieldReturn(Statement body, SourceUnit source) {
+ boolean[] found = {false};
+ new ClassCodeVisitorSupport() {
+ @Override
+ protected SourceUnit getSourceUnit() { return source; }
+
+ @Override
+ public void visitExpressionStatement(ExpressionStatement stmt) {
+ Expression expr = stmt.getExpression();
+ if (expr instanceof StaticMethodCallExpression smce
+ && "yieldReturn".equals(smce.getMethod())) {
+ found[0] = true;
+ }
+ super.visitExpressionStatement(stmt);
+ }
+
+ @Override
+ public void visitClosureExpression(ClosureExpression expr) {
+ // Do NOT visit into nested closures — yield return inside
+ // an async { ... } sub-closure is handled by its own wrapping
+ }
+ }.visitMethod(new MethodNode("$check", 0, ClassHelper.VOID_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body));
+ return found[0];
+ }
+
+ /**
+ * Walks the method body and transforms {@code await(expr)} method calls
+ * into {@code AsyncUtils.await(expr)} static calls. With the new grammar
+ * ({@code await expr}), most calls are already compiled to
+ * {@code AsyncUtils.await()} by the parser; this transformer handles the
+ * legacy {@code await(expr)} call-style for backward compatibility.
+ */
+ 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
+ && "await".equals(mce.getMethodAsString())
+ && mce.isImplicitThis()) {
+ Expression transformedArgs = mce.getArguments().transformExpression(this);
+ StaticMethodCallExpression replacement = callX(ASYNC_UTILS_TYPE, "await", transformedArgs);
+ replacement.setSourcePosition(expr);
+ return replacement;
+ }
+
+ return expr.transformExpression(this);
+ }
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
new file mode 100644
index 0000000000..80f7e2f9e9
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
@@ -0,0 +1,2108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Comprehensive syntax-level tests for the {@code async}/{@code await}/{@code for await}
+ * language features. Every test uses {@code assertScript} to exercise the actual
+ * Groovy parser and AST transformation pipeline, ensuring the grammar, AstBuilder,
+ * and runtime all work end-to-end from the developer's perspective.
+ *
+ * <p>Organized by feature area:
+ * <ol>
+ * <li>{@code async} modifier – method declarations</li>
+ * <li>{@code await} expression – in various statement positions</li>
+ * <li>{@code for await} – async iteration</li>
+ * <li>Combined patterns – real-world usage scenarios</li>
+ * <li>Backward compatibility – {@code async}/{@code await} as identifiers</li>
+ * <li>Error cases – syntax and semantic rejections</li>
+ * </ol>
+ */
+class AsyncAwaitSyntaxTest {
+
+ // =====================================================================
+ // 1. 'async' modifier — method declarations
+ // =====================================================================
+
+ @Test
+ void testAsyncDefMethodReturnsAwaitable() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async greet() { "hello" }
+ }
+ def r = new Svc().greet()
+ assert r instanceof Awaitable // its default implementation is `groovy.concurrent.GroovyPromise`
+ assert r.get() == "hello"
+ '''
+ }
+
+ @Test
+ void testAsyncTypedReturnMethod() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async String upper(String s) { s.toUpperCase() }
+ }
+ assert new Svc().upper("abc").get() == "ABC"
+ '''
+ }
+
+ @Test
+ void testAsyncIntReturnMethod() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async int square(int n) { n * n }
+ }
+ assert new Svc().square(7).get() == 49
+ '''
+ }
+
+ @Test
+ void testAsyncVoidMethodReturnsAwaitable() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.atomic.AtomicReference
+
+ class Svc {
+ AtomicReference<String> ref = new AtomicReference<>()
+ async void store(String v) { ref.set(v) }
+ }
+ def svc = new Svc()
+ def a = svc.store("done")
+ assert a instanceof Awaitable
+ a.get()
+ assert svc.ref.get() == "done"
+ '''
+ }
+
+ @Test
+ void testAsyncStaticMethod() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Util {
+ async static def add(int a, int b) { a + b }
+ }
+ assert Util.add(3, 4).get() == 7
+ '''
+ }
+
+ @Test
+ void testAsyncDefInScript() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async multiply(int a, int b) { a * b }
+
+ def r = multiply(6, 7)
+ assert r instanceof Awaitable
+ assert r.get() == 42
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithMultipleParams() {
+ assertScript '''
+ class Svc {
+ async concat(String a, String b, String sep) {
+ return a + sep + b
+ }
+ }
+ assert new Svc().concat("foo", "bar", "-").get() == "foo-bar"
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithDefaultParams() {
+ assertScript '''
+ class Svc {
+ async greet(String name, String greeting = "Hello") {
+ "${greeting}, ${name}!"
+ }
+ }
+ def svc = new Svc()
+ assert svc.greet("World").get() == "Hello, World!"
+ assert svc.greet("World", "Hi").get() == "Hi, World!"
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithVarargs() {
+ assertScript '''
+ class Svc {
+ async sum(int... nums) {
+ nums.sum()
+ }
+ }
+ assert new Svc().sum(1, 2, 3, 4).get() == 10
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithClosureParam() {
+ assertScript '''
+ class Svc {
+ async transform(String s, Closure fn) {
+ fn(s)
+ }
+ }
+ assert new Svc().transform("hello") { it.reverse() }.get() == "olleh"
+ '''
+ }
+
+ @Test
+ void testAsyncMethodRunsAsynchronously() {
+ assertScript '''
+ class Svc {
+ async threadName() { Thread.currentThread().name }
+ }
+ def name = new Svc().threadName().get()
+ assert name != Thread.currentThread().name
+ '''
+ }
+
+ // =====================================================================
+ // 2. 'await' expression — in various statement positions
+ // =====================================================================
+
+ @Test
+ void testAwaitCompletableFuture() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async fetch() {
+ def v = await CompletableFuture.supplyAsync { 42 }
+ return v
+ }
+ }
+ assert new Svc().fetch().get() == 42
+ '''
+ }
+
+ @Test
+ void testAwaitAwaitableObject() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async inner() { "from-inner" }
+ async outer() {
+ def v = await inner()
+ return "got: ${v}"
+ }
+ }
+ assert new Svc().outer().get() == "got: from-inner"
+ '''
+ }
+
+ @Test
+ void testAwaitWithParenthesizedExpression() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async compute() {
+ def v = await(CompletableFuture.supplyAsync { 99 })
+ return v
+ }
+ }
+ assert new Svc().compute().get() == 99
+ '''
+ }
+
+ @Test
+ void testAwaitInAssignment() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async work() {
+ String result
+ result = await CompletableFuture.supplyAsync { "assigned" }
+ return result
+ }
+ }
+ assert new Svc().work().get() == "assigned"
+ '''
+ }
+
+ @Test
+ void testAwaitInArithmeticExpression() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async calc() {
+ def a = await CompletableFuture.supplyAsync { 10 }
+ def b = await CompletableFuture.supplyAsync { 20 }
+ return a + b + 5
+ }
+ }
+ assert new Svc().calc().get() == 35
+ '''
+ }
+
+ @Test
+ void testMultipleSequentialAwaits() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async pipeline() {
+ def a = await CompletableFuture.supplyAsync { "step1" }
+ def b = await CompletableFuture.supplyAsync { "${a}->step2" }
+ def c = await CompletableFuture.supplyAsync { "${b}->step3" }
+ return c
+ }
+ }
+ assert new Svc().pipeline().get() == "step1->step2->step3"
+ '''
+ }
+
+ @Test
+ void testAwaitInReturnStatement() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async direct() {
+ return await CompletableFuture.supplyAsync { "direct-return" }
+ }
+ }
+ assert new Svc().direct().get() == "direct-return"
+ '''
+ }
+
+ @Test
+ void testAwaitInIfCondition() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async decide(boolean flag) {
+ if (flag) {
+ return await CompletableFuture.supplyAsync { "yes" }
+ } else {
+ return await CompletableFuture.supplyAsync { "no" }
+ }
+ }
+ }
+ def svc = new Svc()
+ assert svc.decide(true).get() == "yes"
+ assert svc.decide(false).get() == "no"
+ '''
+ }
+
+ @Test
+ void testAwaitInTernaryExpression() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async ternary(boolean flag) {
+ def v = flag ? (await CompletableFuture.supplyAsync { "T" })
+ : (await CompletableFuture.supplyAsync { "F" })
+ return v
+ }
+ }
+ def svc = new Svc()
+ assert svc.ternary(true).get() == "T"
+ assert svc.ternary(false).get() == "F"
+ '''
+ }
+
+ @Test
+ void testAwaitInWhileLoop() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async countdown() {
+ def results = []
+ int n = 3
+ while (n > 0) {
+ def v = await CompletableFuture.supplyAsync { n }
+ results << v
+ n--
+ }
+ return results
+ }
+ }
+ assert new Svc().countdown().get() == [3, 2, 1]
+ '''
+ }
+
+ @Test
+ void testAwaitInForLoop() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async processItems() {
+ def results = []
+ for (int i = 0; i < 4; i++) {
+ def v = await CompletableFuture.supplyAsync { i * 10 }
+ results << v
+ }
+ return results
+ }
+ }
+ assert new Svc().processItems().get() == [0, 10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testAwaitInForEachLoop() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async doubleAll() {
+ def results = []
+ for (x in [1, 2, 3]) {
+ def v = await CompletableFuture.supplyAsync { x * 2 }
+ results << v
+ }
+ return results
+ }
+ }
+ assert new Svc().doubleAll().get() == [2, 4, 6]
+ '''
+ }
+
+ @Test
+ void testAwaitInTryCatch() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async safe() {
+ try {
+ return await CompletableFuture.supplyAsync {
+ throw new RuntimeException("boom")
+ }
+ } catch (RuntimeException e) {
+ return "caught: ${e.message}"
+ }
+ }
+ }
+ assert new Svc().safe().get() == "caught: boom"
+ '''
+ }
+
+ @Test
+ void testAwaitInTryFinally() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.atomic.AtomicBoolean
+
+ class Svc {
+ AtomicBoolean cleaned = new AtomicBoolean(false)
+ async withCleanup() {
+ try {
+ return await CompletableFuture.supplyAsync { "result" }
+ } finally {
+ cleaned.set(true)
+ }
+ }
+ }
+ def svc = new Svc()
+ assert svc.withCleanup().get() == "result"
+ assert svc.cleaned.get()
+ '''
+ }
+
+ @Test
+ void testAwaitInSwitchExpression() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async route(int code) {
+ switch (code) {
+ case 1: return await CompletableFuture.supplyAsync { "one" }
+ case 2: return await CompletableFuture.supplyAsync { "two" }
+ default: return await CompletableFuture.supplyAsync { "other" }
+ }
+ }
+ }
+ def svc = new Svc()
+ assert svc.route(1).get() == "one"
+ assert svc.route(2).get() == "two"
+ assert svc.route(9).get() == "other"
+ '''
+ }
+
+ @Test
+ void testAwaitInStringInterpolation() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async greet() {
+ def name = await CompletableFuture.supplyAsync { "World" }
+ return "Hello, ${name}!"
+ }
+ }
+ assert new Svc().greet().get() == "Hello, World!"
+ '''
+ }
+
+ @Test
+ void testAwaitInCollectionLiteral() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async buildList() {
+ def a = await CompletableFuture.supplyAsync { "x" }
+ def b = await CompletableFuture.supplyAsync { "y" }
+ return [a, b, "z"]
+ }
+ }
+ assert new Svc().buildList().get() == ["x", "y", "z"]
+ '''
+ }
+
+ @Test
+ void testAwaitInMapLiteral() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async buildMap() {
+ def name = await CompletableFuture.supplyAsync { "Alice" }
+ def age = await CompletableFuture.supplyAsync { 30 }
+ return [name: name, age: age]
+ }
+ }
+ assert new Svc().buildMap().get() == [name: "Alice", age: 30]
+ '''
+ }
+
+ @Test
+ void testAwaitAsMethodArgument() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ String format(String s) { "[${s}]" }
+ async process() {
+ def v = await CompletableFuture.supplyAsync { "data" }
+ return format(v)
+ }
+ }
+ assert new Svc().process().get() == "[data]"
+ '''
+ }
+
+ @Test
+ void testNestedAwaitExpressions() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async nested() {
+ // await of (await result used to create another future)
+ def inner = await CompletableFuture.supplyAsync { 5 }
+ def outer = await CompletableFuture.supplyAsync { inner * 10 }
+ return outer
+ }
+ }
+ assert new Svc().nested().get() == 50
+ '''
+ }
+
+ @Test
+ void testAwaitInClosureInsideAsyncMethod() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async transform() {
+ def items = [1, 2, 3]
+ // Note: await inside collect closure — this works because
+ // the closure executes synchronously within the async method
+ def results = items.collect { n ->
+ await CompletableFuture.supplyAsync { n * 100 }
+ }
+ return results
+ }
+ }
+ assert new Svc().transform().get() == [100, 200, 300]
+ '''
+ }
+
+ @Test
+ void testAwaitNullResult() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async fetchNull() {
+ def v = await CompletableFuture.supplyAsync { null }
+ return v
+ }
+ }
+ assert new Svc().fetchNull().get() == null
+ '''
+ }
+
+ @Test
+ void testAwaitBooleanResult() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async check() {
+ boolean ok = await CompletableFuture.supplyAsync { true }
+ return ok
+ }
+ }
+ assert new Svc().check().get() == true
+ '''
+ }
+
+ @Test
+ void testAwaitChainedMethodCallOnResult() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async process() {
+ def s = await CompletableFuture.supplyAsync { "hello world" }
+ return s.toUpperCase().split(" ").toList()
+ }
+ }
+ assert new Svc().process().get() == ["HELLO", "WORLD"]
+ '''
+ }
+
+ // =====================================================================
+ // 3. 'for await' — async iteration
+ // =====================================================================
+
+ @Test
+ void testForAwaitBasicIteration() {
+ assertScript '''
+ class Svc {
+ async collect() {
+ def results = []
+ for await (item in [10, 20, 30]) {
+ results << item
+ }
+ return results
+ }
+ }
+ assert new Svc().collect().get() == [10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testForAwaitBasicIteration_2() {
+ assertScript '''
+ class Svc {
+ async collect() {
+ def results = []
+ for await (item : [10, 20, 30]) {
+ results << item
+ }
+ return results
+ }
+ }
+ assert new Svc().collect().get() == [10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithTransformation() {
+ assertScript '''
+ class Svc {
+ async doubleAll() {
+ def results = []
+ for await (item in [1, 2, 3, 4]) {
+ results << item * 2
+ }
+ return results
+ }
+ }
+ assert new Svc().doubleAll().get() == [2, 4, 6, 8]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithBreak() {
+ assertScript '''
+ class Svc {
+ async findFirst() {
+ for await (item in [5, 10, 15, 20, 25]) {
+ if (item > 12) return item
+ }
+ return -1
+ }
+ }
+ assert new Svc().findFirst().get() == 15
+ '''
+ }
+
+ @Test
+ void testForAwaitWithContinue() {
+ assertScript '''
+ class Svc {
+ async skipOdds() {
+ def results = []
+ for await (item in [1, 2, 3, 4, 5, 6]) {
+ if (item % 2 != 0) continue
+ results << item
+ }
+ return results
+ }
+ }
+ assert new Svc().skipOdds().get() == [2, 4, 6]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithTypedVariable() {
+ assertScript '''
+ class Svc {
+ async typed() {
+ def results = []
+ for await (String s in ["hello", "world"]) {
+ results << s.toUpperCase()
+ }
+ return results
+ }
+ }
+ assert new Svc().typed().get() == ["HELLO", "WORLD"]
+ '''
+ }
+
+ @Test
+ void testForAwaitEmptySource() {
+ assertScript '''
+ class Svc {
+ async empty() {
+ def results = []
+ for await (item in []) {
+ results << item
+ }
+ return results
+ }
+ }
+ assert new Svc().empty().get() == []
+ '''
+ }
+
+ @Test
+ void testForAwaitSingleElement() {
+ assertScript '''
+ class Svc {
+ async single() {
+ def results = []
+ for await (item in ["only"]) {
+ results << item
+ }
+ return results
+ }
+ }
+ assert new Svc().single().get() == ["only"]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithIndex() {
+ assertScript '''
+ class Svc {
+ async indexed() {
+ def results = []
+ int idx = 0
+ for await (item in ["a", "b", "c"]) {
+ results << "${idx}:${item}"
+ idx++
+ }
+ return results
+ }
+ }
+ assert new Svc().indexed().get() == ["0:a", "1:b", "2:c"]
+ '''
+ }
+
+ @Test
+ void testForAwaitNestedInRegularFor() {
+ assertScript '''
+ class Svc {
+ async nested() {
+ def results = []
+ for (prefix in ["A", "B"]) {
+ for await (n in [1, 2]) {
+ results << "${prefix}${n}"
+ }
+ }
+ return results
+ }
+ }
+ assert new Svc().nested().get() == ["A1", "A2", "B1", "B2"]
+ '''
+ }
+
+ @Test
+ void testRegularForNestedInForAwait() {
+ assertScript '''
+ class Svc {
+ async nested() {
+ def results = []
+ for await (base in [10, 20]) {
+ for (int i = 0; i < 3; i++) {
+ results << base + i
+ }
+ }
+ return results
+ }
+ }
+ assert new Svc().nested().get() == [10, 11, 12, 20, 21, 22]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithAwaitInsideBody() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async process() {
+ def results = []
+ for await (item in [1, 2, 3]) {
+ def doubled = await CompletableFuture.supplyAsync { item * 2 }
+ results << doubled
+ }
+ return results
+ }
+ }
+ assert new Svc().process().get() == [2, 4, 6]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithAsyncStream() {
+ assertScript '''
+ import groovy.concurrent.AsyncStream
+ import groovy.concurrent.Awaitable
+
+ class CountStream implements AsyncStream<Integer> {
+ private int current = 0
+ private final int max
+ CountStream(int max) { this.max = max }
+
+ Awaitable<Boolean> moveNext() {
+ current++
+ return Awaitable.of(current <= max)
+ }
+ Integer getCurrent() { current }
+ }
+
+ class Svc {
+ async sum() {
+ int total = 0
+ for await (n in new CountStream(5)) {
+ total += n
+ }
+ return total
+ }
+ }
+ assert new Svc().sum().get() == 15
+ '''
+ }
+
+ @Test
+ void testMultipleForAwaitSequential() {
+ assertScript '''
+ class Svc {
+ async multi() {
+ def r1 = []
+ for await (x in [1, 2]) { r1 << x }
+ def r2 = []
+ for await (y in [3, 4]) { r2 << y }
+ return [r1, r2]
+ }
+ }
+ assert new Svc().multi().get() == [[1, 2], [3, 4]]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithAnnotation() {
+ assertScript '''
+ import groovy.transform.Async
+
+ class Svc {
+ @Async
+ def collectItems() {
+ def results = []
+ for await (item in [100, 200, 300]) {
+ results << item
+ }
+ return results
+ }
+ }
+ assert new Svc().collectItems().get() == [100, 200, 300]
+ '''
+ }
+
+ @Test
+ void testForAwaitInScript() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ @Async
+ def gatherItems() {
+ def results = []
+ for await (item in ["x", "y", "z"]) {
+ results << item
+ }
+ return results
+ }
+
+ assert gatherItems().get() == ["x", "y", "z"]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithTryCatch() {
+ assertScript '''
+ class Svc {
+ async safeIterate() {
+ def results = []
+ try {
+ for await (item in [1, 2, 3]) {
+ results << item
+ }
+ } catch (Exception e) {
+ results << "error"
+ }
+ return results
+ }
+ }
+ assert new Svc().safeIterate().get() == [1, 2, 3]
+ '''
+ }
+
+ // =====================================================================
+ // 4. Combined patterns — real-world usage scenarios
+ // =====================================================================
+
+ @Test
+ void testAsyncServiceCallingAsyncService() {
+ assertScript '''
+ class UserService {
+ async findUser(long id) {
+ return [id: id, name: "User${id}"]
+ }
+ }
+
+ class OrderService {
+ async findOrders(long userId) {
+ return (1..3).collect { [orderId: it, userId: userId] }
+ }
+ }
+
+ class AggregatorService {
+ def userService = new UserService()
+ def orderService = new OrderService()
+
+ async getUserWithOrders(long id) {
+ def user = await userService.findUser(id)
+ def orders = await orderService.findOrders(id)
+ return [user: user, orders: orders]
+ }
+ }
+
+ def agg = new AggregatorService()
+ def result = agg.getUserWithOrders(42).get()
+ assert result.user.name == "User42"
+ assert result.orders.size() == 3
+ assert result.orders[0].userId == 42
+ '''
+ }
+
+ @Test
+ void testAsyncWithExceptionPropagation() {
+ assertScript '''
+ import java.util.concurrent.ExecutionException
+
+ class Svc {
+ async fail() {
+ throw new IllegalArgumentException("bad input")
+ }
+ }
+ try {
+ new Svc().fail().get()
+ assert false : "expected exception"
+ } catch (ExecutionException e) {
+ assert e.cause instanceof IllegalArgumentException
+ assert e.cause.message == "bad input"
+ }
+ '''
+ }
+
+ @Test
+ void testAwaitPropagatesExceptionToCallerAsync() {
+ assertScript '''
+ class Svc {
+ async failInner() {
+ throw new IllegalStateException("inner-fail")
+ }
+ async caller() {
+ try {
+ await failInner()
+ return "should not reach"
+ } catch (IllegalStateException e) {
+ return "recovered: ${e.message}"
+ }
+ }
+ }
+ assert new Svc().caller().get() == "recovered: inner-fail"
+ '''
+ }
+
+ @Test
+ void testAsyncAwaitWithCollectionProcessing() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async processAll(List<Integer> items) {
+ def results = []
+ for (item in items) {
+ def r = await CompletableFuture.supplyAsync { item * item }
+ results << r
+ }
+ return results
+ }
+ }
+ assert new Svc().processAll([1, 2, 3, 4, 5]).get() == [1, 4, 9, 16, 25]
+ '''
+ }
+
+ @Test
+ void testAsyncAwaitProducerConsumer() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Producer {
+ async produce(int n) { n * 10 }
+ }
+ class Consumer {
+ def producer = new Producer()
+ async consume() {
+ def sum = 0
+ for (i in 1..5) {
+ sum += await producer.produce(i)
+ }
+ return sum
+ }
+ }
+ // 10 + 20 + 30 + 40 + 50 = 150
+ assert new Consumer().consume().get() == 150
+ '''
+ }
+
+ @Test
+ void testAsyncAwaitParallelThenGather() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async step(int n) { n * 2 }
+
+ async parallelGather() {
+ // Launch multiple async tasks
+ def f1 = step(1)
+ def f2 = step(2)
+ def f3 = step(3)
+ // Await all results
+ return [await f1, await f2, await f3]
+ }
+ }
+ assert new Svc().parallelGather().get() == [2, 4, 6]
+ '''
+ }
+
+ @Test
+ void testAsyncAwaitChainedTransformation() {
+ assertScript '''
+ class Pipeline {
+ async step1(String input) { input.toUpperCase() }
+ async step2(String input) { input.reverse() }
+ async step3(String input) { "[${input}]" }
+
+ async run(String input) {
+ def r1 = await step1(input)
+ def r2 = await step2(r1)
+ return await step3(r2)
+ }
+ }
+ assert new Pipeline().run("abc").get() == "[CBA]"
+ '''
+ }
+
+ @Test
+ void testAsyncMixedAnnotationAndKeyword() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ @Async
+ def fromAnnotation() { "annotated" }
+
+ async fromKeyword() { "keyword" }
+
+ async combine() {
+ def a = await fromAnnotation()
+ def b = await fromKeyword()
+ return "${a}+${b}"
+ }
+ }
+ assert new Svc().combine().get() == "annotated+keyword"
+ '''
+ }
+
+ @Test
+ void testAsyncWithConditionalAwait() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async fetchIf(boolean shouldFetch) {
+ if (shouldFetch) {
+ return await CompletableFuture.supplyAsync { "fetched" }
+ }
+ return "cached"
+ }
+ }
+ def svc = new Svc()
+ assert svc.fetchIf(true).get() == "fetched"
+ assert svc.fetchIf(false).get() == "cached"
+ '''
+ }
+
+ @Test
+ void testAsyncRecursion() {
+ assertScript '''
+ class Svc {
+ async factorial(int n) {
+ if (n <= 1) return 1
+ def sub = await factorial(n - 1)
+ return n * sub
+ }
+ }
+ assert new Svc().factorial(5).get() == 120
+ '''
+ }
+
+ @Test
+ void testAsyncWithRetry() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.atomic.AtomicInteger
+
+ class Svc {
+ AtomicInteger attempts = new AtomicInteger(0)
+
+ async unreliable() {
+ if (attempts.incrementAndGet() < 3) {
+ throw new RuntimeException("not yet")
+ }
+ return "success"
+ }
+
+ async withRetry(int maxRetries) {
+ for (int i = 0; i < maxRetries; i++) {
+ try {
+ return await unreliable()
+ } catch (RuntimeException e) {
+ if (i == maxRetries - 1) throw e
+ }
+ }
+ }
+ }
+ assert new Svc().withRetry(5).get() == "success"
+ '''
+ }
+
+ @Test
+ void testAsyncWithTimeout() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.TimeUnit
+ import java.util.concurrent.TimeoutException
+
+ class Svc {
+ async slow() {
+ Thread.sleep(2000)
+ return "done"
+ }
+ }
+ def a = new Svc().slow()
+ try {
+ a.get(50, TimeUnit.MILLISECONDS)
+ assert false : "expected timeout"
+ } catch (TimeoutException e) {
+ // expected
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncWithAwaitableOf() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async useOf() {
+ def a = await Awaitable.of(42)
+ return a + 8
+ }
+ }
+ assert new Svc().useOf().get() == 50
+ '''
+ }
+
+ @Test
+ void testAsyncWithAwaitableThen() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async inner() { 10 }
+ async chained() {
+ def a = inner() // returns Awaitable<Integer>
+ def b = a.then { it * 3 }
+ return await b
+ }
+ }
+ assert new Svc().chained().get() == 30
+ '''
+ }
+
+ @Test
+ void testAsyncWithAwaitableThenCompose() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async step1() { 5 }
+ async step2(int v) { v * 4 }
+ async composed() {
+ def a = step1()
+ def b = a.thenCompose { v -> step2(v) }
+ return await b
+ }
+ }
+ assert new Svc().composed().get() == 20
+ '''
+ }
+
+ @Test
+ void testAsyncWithAwaitableExceptionally() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Svc {
+ async failing() {
+ throw new RuntimeException("oops")
+ }
+ async recovered() {
+ def a = failing().exceptionally { "fallback" }
+ return await a
+ }
+ }
+ assert new Svc().recovered().get() == "fallback"
+ '''
+ }
+
+ @Test
+ void testConcurrentAsyncMethodsWithSyntax() {
+ assertScript '''
+ import java.util.concurrent.ConcurrentHashMap
+
+ class Svc {
+ def results = new ConcurrentHashMap()
+ async store(String key, String value) {
+ results.put(key, value)
+ return value
+ }
+ }
+ def svc = new Svc()
+ def futures = (1..10).collect { i -> svc.store("k${i}", "v${i}") }
+ futures.each { it.get() }
+ assert svc.results.size() == 10
+ assert svc.results["k5"] == "v5"
+ '''
+ }
+
+ @Test
+ void testScriptLevelAsyncAwait() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ async fetchData(String key) {
+ return "data-for-${key}"
+ }
+
+ async main() {
+ def r1 = await fetchData("alpha")
+ def r2 = await fetchData("beta")
+ return [r1, r2]
+ }
+
+ def result = main().get()
+ assert result[0] == "data-for-alpha"
+ assert result[1] == "data-for-beta"
+ '''
+ }
+
+ @Test
+ void testScriptLevelForAwait() {
+ assertScript '''
+ async items() {
+ def results = []
+ for await (x in [1, 2, 3]) {
+ results << x * 10
+ }
+ return results
+ }
+ assert items().get() == [10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testAsyncWithCustomExecutor() {
+ assertScript '''
+ import java.util.concurrent.Executors
+
+ class Svc {
+ static executor = Executors.newFixedThreadPool(2)
+ async threadInfo() {
+ return Thread.currentThread().name
+ }
+ }
+
+ groovy.transform.AsyncUtils.setExecutor(Svc.executor)
+ try {
+ def name = new Svc().threadInfo().get()
+ assert name.contains("pool")
+ } finally {
+ groovy.transform.AsyncUtils.setExecutor(null)
+ Svc.executor.shutdown()
+ }
+ '''
+ }
+
+ @Test
+ void testForAwaitCombinedWithAwaitInSameMethod() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async complexFlow() {
+ def prefix = await CompletableFuture.supplyAsync { "item" }
+ def results = []
+ for await (n in [1, 2, 3]) {
+ results << "${prefix}-${n}"
+ }
+ def suffix = await CompletableFuture.supplyAsync { "-done" }
+ return results.join(",") + suffix
+ }
+ }
+ assert new Svc().complexFlow().get() == "item-1,item-2,item-3-done"
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithGStringReturn() {
+ assertScript '''
+ class Svc {
+ async template(String name, int age) {
+ return "Name: ${name}, Age: ${age}"
+ }
+ }
+ assert new Svc().template("Alice", 25).get() == "Name: Alice, Age: 25"
+ '''
+ }
+
+ @Test
+ void testAsyncMethodReturningCollection() {
+ assertScript '''
+ class Svc {
+ async getList() { [1, 2, 3] }
+ async getMap() { [a: 1, b: 2] }
+ async getSet() { [1, 2, 3] as Set }
+
+ async all() {
+ def list = await getList()
+ def map = await getMap()
+ def set = await getSet()
+ return [list: list, map: map, setSize: set.size()]
+ }
+ }
+ def r = new Svc().all().get()
+ assert r.list == [1, 2, 3]
+ assert r.map == [a: 1, b: 2]
+ assert r.setSize == 3
+ '''
+ }
+
+ @Test
+ void testAsyncWithPropertyAccess() {
+ assertScript '''
+ class User {
+ String name
+ int age
+ }
+
+ class Svc {
+ async getUser() {
+ new User(name: "Bob", age: 40)
+ }
+ async getUserName() {
+ def user = await getUser()
+ return user.name
+ }
+ }
+ assert new Svc().getUserName().get() == "Bob"
+ '''
+ }
+
+ @Test
+ void testAsyncWithSpreadOperator() {
+ assertScript '''
+ class Svc {
+ async getItems() { [[v: 1], [v: 2], [v: 3]] }
+ async getValues() {
+ def items = await getItems()
+ return items*.v
+ }
+ }
+ assert new Svc().getValues().get() == [1, 2, 3]
+ '''
+ }
+
+ @Test
+ void testAsyncWithElvisOperator() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Svc {
+ async fetchOrDefault() {
+ def v = await CompletableFuture.supplyAsync { null }
+ return v ?: "default"
+ }
+ }
+ assert new Svc().fetchOrDefault().get() == "default"
+ '''
+ }
+
+ @Test
+ void testAsyncWithSafeNavigation() {
+ assertScript '''
+ class Svc {
+ async nullResult() { null }
+ async safe() {
+ def r = await nullResult()
+ return r?.toString() ?: "was-null"
+ }
+ }
+ assert new Svc().safe().get() == "was-null"
+ '''
+ }
+
+ // =====================================================================
+ // 5. Backward compatibility — async/await as identifiers
+ // =====================================================================
+
+ @Test
+ void testAsyncAsLocalVariable() {
+ assertScript '''
+ def async = "value"
+ assert async == "value"
+ '''
+ }
+
+ @Test
+ void testAwaitAsLocalVariable() {
+ assertScript '''
+ def await = 123
+ assert await == 123
+ '''
+ }
+
+ @Test
+ void testAsyncAsMethodNameInClass() {
+ assertScript '''
+ class Legacy {
+ def async() { "legacy-async" }
+ def await() { "legacy-await" }
+ }
+ def obj = new Legacy()
+ assert obj.async() == "legacy-async"
+ assert obj.await() == "legacy-await"
+ '''
+ }
+
+ @Test
+ void testAsyncAsFieldName() {
+ assertScript '''
+ class Config {
+ boolean async = true
+ String await = "ready"
+ }
+ def c = new Config()
+ assert c.async == true
+ assert c.await == "ready"
+ '''
+ }
+
+ @Test
+ void testAsyncAsMapKey() {
+ assertScript '''
+ def map = [async: true, await: false]
+ assert map.async == true
+ assert map.await == false
+ '''
+ }
+
+ @Test
+ void testAsyncAsParameterName() {
+ assertScript '''
+ def process(boolean async) { async ? "yes" : "no" }
+ assert process(true) == "yes"
+ '''
+ }
+
+ @Test
+ void testAsyncAndAwaitAsLabels() {
+ assertScript '''
+ def result = null
+ async:
+ for (i in [1, 2, 3]) {
+ await:
+ for (j in [10, 20]) {
+ if (j == 20) {
+ result = i * j
+ break async
+ }
+ }
+ }
+ assert result == 20
+ '''
+ }
+
+ // =====================================================================
+ // 6. Error cases — syntax and semantic rejections
+ // =====================================================================
+
+ @Test
+ void testAsyncOnAbstractMethodFails() {
+ shouldFail '''
+ abstract class Svc {
+ @groovy.transform.Async
+ abstract def compute()
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncOnAwaitableReturnTypeFails() {
+ shouldFail '''
+ import groovy.concurrent.Awaitable
+ class Svc {
+ @groovy.transform.Async
+ Awaitable<String> compute() { Awaitable.of("x") }
+ }
+ '''
+ }
+
+ @Test
+ void testForAwaitWithTraditionalForSyntaxFails() {
+ shouldFail '''
+ class Svc {
+ @groovy.transform.Async
+ def work() {
+ for await (int i = 0; i < 10; i++) {
+ println i
+ }
+ }
+ }
+ '''
+ }
+
+ // =====================================================================
+ // 7. 'yield return' — async generator (C#-style async streams)
+ // =====================================================================
+
+ @Test
+ void testYieldReturnBasic() {
+ assertScript '''
+ import groovy.concurrent.AsyncStream
+
+ class Gen {
+ async numbers() {
+ yield return 1
+ yield return 2
+ yield return 3
+ }
+ }
+ def stream = new Gen().numbers()
+ assert stream instanceof AsyncStream
+ def results = []
+ for await (n in stream) {
+ results << n
+ }
+ assert results == [1, 2, 3]
+ '''
+ }
+
+ @Test
+ void testYieldReturnInForLoop() {
+ assertScript '''
+ class Gen {
+ async range(int start, int end) {
+ for (int i = start; i <= end; i++) {
+ yield return i
+ }
+ }
+ }
+ def results = []
+ for await (n in new Gen().range(1, 5)) {
+ results << n
+ }
+ assert results == [1, 2, 3, 4, 5]
+ '''
+ }
+
+ @Test
+ void testYieldReturnInWhileLoop() {
+ assertScript '''
+ class Gen {
+ async countdown(int n) {
+ while (n > 0) {
+ yield return n
+ n--
+ }
+ }
+ }
+ def results = []
+ for await (v in new Gen().countdown(4)) {
+ results << v
+ }
+ assert results == [4, 3, 2, 1]
+ '''
+ }
+
+ @Test
+ void testYieldReturnInForEach() {
+ assertScript '''
+ class Gen {
+ async transform(List items) {
+ for (item in items) {
+ yield return item.toString().toUpperCase()
+ }
+ }
+ }
+ def results = []
+ for await (s in new Gen().transform(["hello", "world"])) {
+ results << s
+ }
+ assert results == ["HELLO", "WORLD"]
+ '''
+ }
+
+ @Test
+ void testYieldReturnStrings() {
+ assertScript '''
+ class Gen {
+ async greetings() {
+ yield return "hello"
+ yield return "world"
+ yield return "!"
+ }
+ }
+ def results = []
+ for await (s in new Gen().greetings()) {
+ results << s
+ }
+ assert results == ["hello", "world", "!"]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithGString() {
+ assertScript '''
+ class Gen {
+ async labeled(List items) {
+ int idx = 0
+ for (item in items) {
+ yield return "${idx}: ${item}"
+ idx++
+ }
+ }
+ }
+ def results = []
+ for await (s in new Gen().labeled(["a", "b", "c"])) {
+ results << s
+ }
+ assert results == ["0: a", "1: b", "2: c"]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithFilter() {
+ assertScript '''
+ class Gen {
+ async evens(int max) {
+ for (int i = 1; i <= max; i++) {
+ if (i % 2 == 0) {
+ yield return i
+ }
+ }
+ }
+ }
+ def results = []
+ for await (n in new Gen().evens(10)) {
+ results << n
+ }
+ assert results == [2, 4, 6, 8, 10]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithParameters() {
+ assertScript '''
+ class Gen {
+ async multiply(List<Integer> items, int factor) {
+ for (item in items) {
+ yield return item * factor
+ }
+ }
+ }
+ def results = []
+ for await (n in new Gen().multiply([1, 2, 3], 10)) {
+ results << n
+ }
+ assert results == [10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testYieldReturnEmpty() {
+ assertScript '''
+ class Gen {
+ async empty() {
+ // A generator must have at least one yield return
+ // to be detected as a generator. An empty body with
+ // no yield returns becomes a regular async method.
+ if (false) yield return "unreachable"
+ }
+ }
+ def results = []
+ for await (item in new Gen().empty()) {
+ results << item
+ }
+ assert results == []
+ '''
+ }
+
+ @Test
+ void testYieldReturnSingleElement() {
+ assertScript '''
+ class Gen {
+ async single() {
+ yield return "only"
+ }
+ }
+ def results = []
+ for await (item in new Gen().single()) {
+ results << item
+ }
+ assert results == ["only"]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithConditionalLogic() {
+ assertScript '''
+ class Gen {
+ async fizzBuzz(int n) {
+ for (int i = 1; i <= n; i++) {
+ if (i % 15 == 0) yield return "FizzBuzz"
+ else if (i % 3 == 0) yield return "Fizz"
+ else if (i % 5 == 0) yield return "Buzz"
+ else yield return i.toString()
+ }
+ }
+ }
+ def results = []
+ for await (s in new Gen().fizzBuzz(15)) {
+ results << s
+ }
+ assert results[0] == "1"
+ assert results[2] == "Fizz"
+ assert results[4] == "Buzz"
+ assert results[14] == "FizzBuzz"
+ assert results.size() == 15
+ '''
+ }
+
+ @Test
+ void testYieldReturnConsumerBreaksEarly() {
+ assertScript '''
+ class Gen {
+ async infinite() {
+ int i = 0
+ while (true) {
+ yield return i++
+ }
+ }
+ }
+ // Consumer only takes first 5
+ def results = []
+ def stream = new Gen().infinite()
+ for await (n in stream) {
+ results << n
+ if (results.size() >= 5) break
+ }
+ assert results == [0, 1, 2, 3, 4]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithAnnotation() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.AsyncStream
+
+ class Gen {
+ @Async
+ def items() {
+ yield return "x"
+ yield return "y"
+ }
+ }
+ def stream = new Gen().items()
+ assert stream instanceof AsyncStream
+ def results = []
+ for await (s in stream) {
+ results << s
+ }
+ assert results == ["x", "y"]
+ '''
+ }
+
+ @Test
+ void testYieldReturnInScript() {
+ assertScript '''
+ import groovy.concurrent.AsyncStream
+
+ async fibonacci(int count) {
+ int a = 0, b = 1
+ for (int i = 0; i < count; i++) {
+ yield return a
+ int temp = a + b
+ a = b
+ b = temp
+ }
+ }
+
+ def results = []
+ for await (n in fibonacci(8)) {
+ results << n
+ }
+ assert results == [0, 1, 1, 2, 3, 5, 8, 13]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithAwaitInsideBody() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Gen {
+ async asyncItems() {
+ for (i in 1..3) {
+ def v = await CompletableFuture.supplyAsync { i * 100 }
+ yield return v
+ }
+ }
+ }
+ def results = []
+ for await (n in new Gen().asyncItems()) {
+ results << n
+ }
+ assert results == [100, 200, 300]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithTryCatch() {
+ assertScript '''
+ class Gen {
+ async safeItems() {
+ for (i in 1..5) {
+ try {
+ if (i == 3) throw new RuntimeException("skip")
+ yield return i
+ } catch (RuntimeException e) {
+ yield return -1
+ }
+ }
+ }
+ }
+ def results = []
+ for await (n in new Gen().safeItems()) {
+ results << n
+ }
+ assert results == [1, 2, -1, 4, 5]
+ '''
+ }
+
+ @Test
+ void testYieldReturnExceptionPropagation() {
+ assertScript '''
+ class Gen {
+ async failing() {
+ yield return 1
+ yield return 2
+ throw new IllegalStateException("generator failed")
+ }
+ }
+ def results = []
+ try {
+ for await (n in new Gen().failing()) {
+ results << n
+ }
+ assert false : "expected exception"
+ } catch (IllegalStateException e) {
+ assert e.message == "generator failed"
+ }
+ assert results == [1, 2]
+ '''
+ }
+
+ @Test
+ void testYieldReturnNullValues() {
+ assertScript '''
+ class Gen {
+ async withNulls() {
+ yield return "a"
+ yield return null
+ yield return "b"
+ }
+ }
+ def results = []
+ for await (item in new Gen().withNulls()) {
+ results << item
+ }
+ assert results == ["a", null, "b"]
+ '''
+ }
+
+ @Test
+ void testYieldReturnMixedTypes() {
+ assertScript '''
+ class Gen {
+ async mixed() {
+ yield return 42
+ yield return "hello"
+ yield return [1, 2, 3]
+ yield return true
+ }
+ }
+ def results = []
+ for await (item in new Gen().mixed()) {
+ results << item
+ }
+ assert results[0] == 42
+ assert results[1] == "hello"
+ assert results[2] == [1, 2, 3]
+ assert results[3] == true
+ '''
+ }
+
+ @Test
+ void testYieldReturnForAwaitAccumulation() {
+ assertScript '''
+ class Gen {
+ async items() {
+ for (i in 1..5) {
+ yield return i
+ }
+ }
+ }
+ // Sum using for-await
+ async sumStream() {
+ int total = 0
+ for await (n in new Gen().items()) {
+ total += n
+ }
+ return total
+ }
+ assert sumStream().get() == 15
+ '''
+ }
+
+ @Test
+ void testYieldReturnChainedGenerators() {
+ assertScript '''
+ class Gen {
+ async source() {
+ yield return 1
+ yield return 2
+ yield return 3
+ }
+
+ async doubled() {
+ for await (n in source()) {
+ yield return n * 2
+ }
+ }
+ }
+ def results = []
+ for await (n in new Gen().doubled()) {
+ results << n
+ }
+ assert results == [2, 4, 6]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithForAwaitTransformPipeline() {
+ assertScript '''
+ class Pipeline {
+ async numbers(int n) {
+ for (int i = 1; i <= n; i++) {
+ yield return i
+ }
+ }
+
+ async squared(source) {
+ for await (n in source) {
+ yield return n * n
+ }
+ }
+
+ async asStrings(source) {
+ for await (n in source) {
+ yield return "val=${n}"
+ }
+ }
+ }
+ def p = new Pipeline()
+ def results = []
+ for await (s in p.asStrings(p.squared(p.numbers(4)))) {
+ results << s
+ }
+ assert results == ["val=1", "val=4", "val=9", "val=16"]
+ '''
+ }
+
+ @Test
+ void testYieldReturnStaticMethod() {
+ assertScript '''
+ class Gen {
+ async static def staticRange(int n) {
+ for (int i = 0; i < n; i++) {
+ yield return i
+ }
+ }
+ }
+ def results = []
+ for await (n in Gen.staticRange(4)) {
+ results << n
+ }
+ assert results == [0, 1, 2, 3]
+ '''
+ }
+
+ @Test
+ void testYieldReturnWithMapEntries() {
+ assertScript '''
+ class Gen {
+ async entries(Map map) {
+ for (entry in map.entrySet()) {
+ yield return [key: entry.key, value: entry.value]
+ }
+ }
+ }
+ def results = []
+ for await (e in new Gen().entries([a: 1, b: 2, c: 3])) {
+ results << e
+ }
+ assert results.size() == 3
+ assert results.find { it.key == "b" }.value == 2
+ '''
+ }
+
+ @Test
+ void testYieldReturnLargeSequence() {
+ assertScript '''
+ class Gen {
+ async sequence(int count) {
+ for (int i = 0; i < count; i++) {
+ yield return i
+ }
+ }
+ }
+ int sum = 0
+ int count = 0
+ for await (n in new Gen().sequence(1000)) {
+ sum += n
+ count++
+ }
+ assert count == 1000
+ assert sum == (999 * 1000) / 2
+ '''
+ }
+
+ @Test
+ void testYieldReturnOutsideAsyncMethodFails() {
+ assertScript '''
+ // yield return outside async method should fail at runtime
+ try {
+ groovy.transform.AsyncUtils.yieldReturn("oops")
+ assert false : "expected exception"
+ } catch (IllegalStateException e) {
+ assert e.message.contains("yield return")
+ }
+ '''
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncClosureLambdaTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncClosureLambdaTest.groovy
new file mode 100644
index 0000000000..7a717cb20b
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncClosureLambdaTest.groovy
@@ -0,0 +1,793 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Tests for async closures and async lambda expressions.
+ * <p>
+ * Syntax:
+ * <ul>
+ * <li>{@code async { body }} — immediately executes body async, returns Awaitable</li>
+ * <li>{@code async { x, y -> body }} — creates async closure, returns Closure<Awaitable></li>
+ * <li>{@code async (x, y) -> { body }} — creates async lambda, returns Closure<Awaitable></li>
+ * <li>Both support {@code yield return} for AsyncStream generation</li>
+ * </ul>
+ *
+ * @since 6.0.0
+ */
+final class AsyncClosureLambdaTest {
+
+ // =========================================================================
+ // 1. Async Closure — immediate execution (no params)
+ // =========================================================================
+
+ @Test
+ void testAsyncClosureImmediateExecution() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def result = async { 42 }
+ assert result instanceof Awaitable
+ assert await(result) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithExplicitEmptyParams() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def result = async { -> "hello" }
+ assert result instanceof Awaitable
+ assert await(result) == "hello"
+ '''
+ }
+
+ @Test
+ void testAsyncClosureRunsOnSeparateThread() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def mainThread = Thread.currentThread().name
+ def awaitable = async { Thread.currentThread().name }
+ def asyncThread = await(awaitable)
+ assert asyncThread != mainThread
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithComputation() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def awaitable = async {
+ def sum = 0
+ for (i in 1..100) { sum += i }
+ sum
+ }
+ assert await(awaitable) == 5050
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithAwaitInside() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def fetchValue() { return 10 }
+
+ def result = async {
+ def v = await(fetchValue())
+ v * 2
+ }
+ assert await(result) == 20
+ '''
+ }
+
+ @Test
+ void testMultipleAsyncClosuresParallel() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def a = async { Thread.sleep(50); 1 }
+ def b = async { Thread.sleep(50); 2 }
+ def c = async { Thread.sleep(50); 3 }
+
+ def results = awaitAll(a, b, c)
+ assert results == [1, 2, 3]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureAssignedToVariable() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def task = async { 100 + 200 }
+ // Can use the Awaitable later
+ assert task instanceof Awaitable
+ def val = await(task)
+ assert val == 300
+ '''
+ }
+
+ @Test
+ void testAsyncClosureReturnsNull() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def result = async { null }
+ assert await(result) == null
+ '''
+ }
+
+ // =========================================================================
+ // 2. Async Closure — with parameters (deferred execution)
+ // =========================================================================
+
+ @Test
+ void testAsyncClosureWithParams() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncAdd = async { x, y -> x + y }
+ def result = asyncAdd(10, 20)
+ assert await(result) == 30
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithSingleParam() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncDouble = async { n -> n * 2 }
+ assert await(asyncDouble(21)) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithParamsCalledMultipleTimes() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncSquare = async { n -> n * n }
+ def r1 = await(asyncSquare(3))
+ def r2 = await(asyncSquare(5))
+ def r3 = await(asyncSquare(7))
+ assert r1 == 9
+ assert r2 == 25
+ assert r3 == 49
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithParamsIsClosure() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ def asyncFn = async { x -> x.toUpperCase() }
+ assert asyncFn instanceof Closure
+ def result = asyncFn("hello")
+ assert result instanceof Awaitable
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithParamsAndStringOps() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncConcat = async { a, b, sep -> "${a}${sep}${b}" }
+ assert await(asyncConcat("hello", "world", " ")) == "hello world"
+ '''
+ }
+
+ @Test
+ void testAsyncClosurePassedAsArgument() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def applyAsync(fn, x) {
+ return await(fn(x))
+ }
+
+ def asyncInc = async { n -> n + 1 }
+ assert applyAsync(asyncInc, 41) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncClosureStoredInCollection() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def ops = [
+ add: async { a, b -> a + b },
+ mul: async { a, b -> a * b },
+ sub: async { a, b -> a - b }
+ ]
+
+ assert await(ops.add(3, 4)) == 7
+ assert await(ops.mul(3, 4)) == 12
+ assert await(ops.sub(10, 4)) == 6
+ '''
+ }
+
+ // =========================================================================
+ // 3. Async Lambda Expression
+ // =========================================================================
+
+ @Test
+ void testAsyncLambdaWithSingleParam() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncDouble = async (n) -> { n * 2 }
+ assert await(asyncDouble(21)) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncLambdaWithMultipleParams() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncAdd = async (a, b) -> { a + b }
+ assert await(asyncAdd(10, 32)) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncLambdaWithSingleExpression() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncInc = async (n) -> n + 1
+ assert await(asyncInc(41)) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncLambdaIsClosure() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ def fn = async (x) -> { x * x }
+ assert fn instanceof Closure
+ assert fn(5) instanceof Awaitable
+ '''
+ }
+
+ @Test
+ void testAsyncLambdaCalledMultipleTimes() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def factorial = async (n) -> {
+ def result = 1
+ for (i in 2..n) { if (n >= 2) result *= i }
+ result
+ }
+
+ assert await(factorial(5)) == 120
+ assert await(factorial(6)) == 720
+ assert await(factorial(10)) == 3628800
+ '''
+ }
+
+ // =========================================================================
+ // 4. Async Closure with yield return (AsyncStream generator)
+ // =========================================================================
+
+ @Test
+ void testAsyncClosureGeneratorImmediate() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.AsyncStream
+
+ def stream = async {
+ yield return 1
+ yield return 2
+ yield return 3
+ }
+ assert stream instanceof AsyncStream
+
+ def items = []
+ while (await(stream.moveNext())) {
+ items << stream.current
+ }
+ assert items == [1, 2, 3]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureGeneratorWithForAwait() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def collect() {
+ def stream = async {
+ yield return "a"
+ yield return "b"
+ yield return "c"
+ }
+ def items = []
+ for await (item in stream) {
+ items << item
+ }
+ return items
+ }
+
+ assert await(collect()) == ["a", "b", "c"]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureGeneratorWithParams() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def collect() {
+ def range = async { start, end ->
+ for (i in start..end) {
+ yield return i
+ }
+ }
+ def items = []
+ for await (item in range(1, 5)) {
+ items << item
+ }
+ return items
+ }
+
+ assert await(collect()) == [1, 2, 3, 4, 5]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureGeneratorWithParamsCalledMultipleTimes() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def test() {
+ def repeat = async { value, times ->
+ for (i in 1..times) {
+ yield return value
+ }
+ }
+ def r1 = []
+ for await (item in repeat("x", 3)) { r1 << item }
+ def r2 = []
+ for await (item in repeat("y", 2)) { r2 << item }
+ return [r1, r2]
+ }
+
+ def result = await(test())
+ assert result[0] == ["x", "x", "x"]
+ assert result[1] == ["y", "y"]
+ '''
+ }
+
+ // =========================================================================
+ // 5. Async Lambda with yield return
+ // =========================================================================
+
+ @Test
+ void testAsyncLambdaGeneratorWithForAwait() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def collect() {
+ def squares = async (n) -> {
+ for (i in 1..n) {
+ yield return i * i
+ }
+ }
+ def items = []
+ for await (item in squares(4)) {
+ items << item
+ }
+ return items
+ }
+
+ assert await(collect()) == [1, 4, 9, 16]
+ '''
+ }
+
+ // =========================================================================
+ // 6. Exception handling in async closures/lambdas
+ // =========================================================================
+
+ @Test
+ void testAsyncClosureExceptionTransparency() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def caller() {
+ def task = async { throw new java.io.IOException("async closure error") }
+ try {
+ await task
+ assert false
+ } catch (java.io.IOException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "async closure error"
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithParamsExceptionTransparency() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def caller() {
+ def asyncDiv = async { a, b ->
+ if (b == 0) throw new ArithmeticException("division by zero")
+ a / b
+ }
+ try {
+ await asyncDiv(10, 0)
+ assert false
+ } catch (ArithmeticException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "division by zero"
+ '''
+ }
+
+ @Test
+ void testAsyncLambdaExceptionHandling() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def caller() {
+ def failOp = async (msg) -> { throw new IllegalStateException(msg) }
+ try {
+ await failOp("lambda error")
+ assert false
+ } catch (IllegalStateException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "lambda error"
+ '''
+ }
+
+ @Test
+ void testAsyncClosureGeneratorExceptionPropagation() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def caller() {
+ def stream = async {
+ yield return 1
+ throw new java.io.IOException("generator error")
+ }
+ def items = []
+ try {
+ for await (item in stream) {
+ items << item
+ }
+ assert false
+ } catch (java.io.IOException e) {
+ return "${items}:${e.message}"
+ }
+ }
+
+ assert await(caller()) == "[1]:generator error"
+ '''
+ }
+
+ // =========================================================================
+ // 7. Async closure/lambda used with await syntax
+ // =========================================================================
+
+ @Test
+ void testAwaitAsyncClosureInAsyncMethod() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def compute() {
+ def a = await async { 10 }
+ def b = await async { 20 }
+ return a + b
+ }
+
+ assert await(compute()) == 30
+ '''
+ }
+
+ @Test
+ void testAwaitAsyncClosureWithParamsInAsyncMethod() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def compute() {
+ def multiply = async { x, y -> x * y }
+ def a = await multiply(6, 7)
+ return a
+ }
+
+ assert await(compute()) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncClosuresComposed() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def pipeline() {
+ def step1 = async { 10 }
+ def v1 = await step1
+
+ def step2 = async { v1 * 2 }
+ def v2 = await step2
+
+ def step3 = async { v2 + 1 }
+ return await step3
+ }
+
+ assert await(pipeline()) == 21
+ '''
+ }
+
+ // =========================================================================
+ // 8. Async closure/lambda with complex types
+ // =========================================================================
+
+ @Test
+ void testAsyncClosureWithListResult() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def task = async { [1, 2, 3].collect { it * it } }
+ assert await(task) == [1, 4, 9]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithMapResult() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def task = async { [name: "Groovy", version: 6] }
+ def result = await(task)
+ assert result.name == "Groovy"
+ assert result.version == 6
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithStringInterpolation() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def greet = async { name -> "Hello, ${name}!" }
+ assert await(greet("World")) == "Hello, World!"
+ '''
+ }
+
+ // =========================================================================
+ // 9. Async closure/lambda used with awaitAll/awaitAny
+ // =========================================================================
+
+ @Test
+ void testAwaitAllWithAsyncClosures() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def results = awaitAll(
+ async { 1 },
+ async { 2 },
+ async { 3 }
+ )
+ assert results == [1, 2, 3]
+ '''
+ }
+
+ @Test
+ void testAwaitAnyWithAsyncClosures() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def result = awaitAny(
+ async { Thread.sleep(100); "slow" },
+ async { "fast" }
+ )
+ // "fast" should usually win, but either is valid
+ assert result == "fast" || result == "slow"
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettledWithAsyncClosures() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def results = awaitAllSettled(
+ async { 42 },
+ async { throw new RuntimeException("fail") }
+ )
+ assert results[0].isSuccess()
+ assert results[0].value == 42
+ assert results[1].isFailure()
+ assert results[1].error instanceof RuntimeException
+ '''
+ }
+
+ // =========================================================================
+ // 10. Edge cases
+ // =========================================================================
+
+ @Test
+ void testAsyncClosureCapturingOuterVariable() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def x = 100
+ def task = async { x + 1 }
+ assert await(task) == 101
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithParamCapturingOuter() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def base = 1000
+ def asyncAdd = async { n -> base + n }
+ assert await(asyncAdd(42)) == 1042
+ '''
+ }
+
+ @Test
+ void testNestedAsyncClosures() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def outer = async {
+ def inner = async { 42 }
+ await(inner)
+ }
+ assert await(outer) == 42
+ '''
+ }
+
+ @Test
+ void testAsyncClosureInLoopBody() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def tasks = (1..5).collect { n -> async { n * n } }
+ def results = awaitAll(tasks as Object[])
+ assert results == [1, 4, 9, 16, 25]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithMethodCallChaining() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def task = async { "hello world" }
+ assert task instanceof Awaitable
+ def chained = task.then { it.toUpperCase() }
+ assert await(chained) == "HELLO WORLD"
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithExceptionally() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def task = async { throw new RuntimeException("boom") }
+ def recovered = task.exceptionally { err -> "recovered: ${err.message}" }
+ assert await(recovered) == "recovered: boom"
+ '''
+ }
+
+ @Test
+ void testAsyncClosureGeneratorFactoryPattern() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ @Async
+ def collect() {
+ // Factory that creates fibonacci async streams
+ def fibonacci = async { limit ->
+ def a = 0, b = 1
+ for (i in 0..<limit) {
+ yield return a
+ def temp = a + b
+ a = b
+ b = temp
+ }
+ }
+ def items = []
+ for await (item in fibonacci(8)) {
+ items << item
+ }
+ return items
+ }
+
+ assert await(collect()) == [0, 1, 1, 2, 3, 5, 8, 13]
+ '''
+ }
+
+ @Test
+ void testAsyncClosureCancellation() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CancellationException
+
+ def task = async { Thread.sleep(10_000); 42 }
+ task.cancel()
+ assert task.isCancelled()
+ assert task.isCompletedExceptionally()
+ try {
+ await(task)
+ assert false
+ } catch (CancellationException e) {
+ // expected
+ }
+ '''
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncExceptionHandlingTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncExceptionHandlingTest.groovy
new file mode 100644
index 0000000000..e63783ee73
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncExceptionHandlingTest.groovy
@@ -0,0 +1,1161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Comprehensive exception handling tests for async/await, covering:
+ * <ol>
+ * <li>Exception transparency — checked exceptions pass through {@code await}
+ * without wrapping (like C#/Kotlin/JavaScript)</li>
+ * <li>Deep unwrapping — nested CompletionException/ExecutionException chains</li>
+ * <li>Cancellation support — {@code cancel()}/{@code isCancelled()}</li>
+ * <li>{@code awaitAll}/{@code awaitAny} with mixed success/failure</li>
+ * <li>{@code awaitAllSettled} — JavaScript's {@code Promise.allSettled()}</li>
+ * <li>Generator (yield return) exception propagation</li>
+ * <li>Exception chaining across multiple await calls</li>
+ * <li>try/catch/finally in async methods</li>
+ * </ol>
+ *
+ * @since 6.0.0
+ */
+final class AsyncExceptionHandlingTest {
+
+ // =========================================================================
+ // 1. Exception Transparency — checked exceptions through await
+ // =========================================================================
+
+ @Test
+ void testAwaitIOExceptionTransparency() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async fetchData() {
+ throw new java.io.IOException("disk failure")
+ }
+
+ async caller() {
+ try {
+ await fetchData()
+ assert false : "should not reach"
+ } catch (java.io.IOException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "disk failure"
+ '''
+ }
+
+ @Test
+ void testAwaitSQLExceptionTransparency() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async queryDb() {
+ throw new java.sql.SQLException("connection refused", "08001")
+ }
+
+ async caller() {
+ try {
+ await queryDb()
+ assert false : "should not reach"
+ } catch (java.sql.SQLException e) {
+ return e.SQLState
+ }
+ }
+
+ assert await(caller()) == "08001"
+ '''
+ }
+
+ @Test
+ void testAwaitCustomCheckedExceptionTransparency() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class BusinessException extends Exception {
+ int code
+ BusinessException(String msg, int code) {
+ super(msg)
+ this.code = code
+ }
+ }
+
+ async process() {
+ throw new BusinessException("insufficient funds", 402)
+ }
+
+ async caller() {
+ try {
+ await process()
+ assert false
+ } catch (BusinessException e) {
+ return "${e.message}:${e.code}"
+ }
+ }
+
+ assert await(caller()) == "insufficient funds:402"
+ '''
+ }
+
+ @Test
+ void testAwaitRuntimeExceptionTransparency() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async badOp() {
+ throw new IllegalArgumentException("bad arg")
+ }
+
+ async caller() {
+ try {
+ await badOp()
+ assert false
+ } catch (IllegalArgumentException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "bad arg"
+ '''
+ }
+
+ @Test
+ void testAwaitErrorPropagation() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async oom() {
+ throw new OutOfMemoryError("test OOM")
+ }
+
+ async caller() {
+ try {
+ await oom()
+ assert false
+ } catch (OutOfMemoryError e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "test OOM"
+ '''
+ }
+
+ // =========================================================================
+ // 2. Deep Unwrapping — nested wrapper chains
+ // =========================================================================
+
+ @Test
+ void testDeepUnwrapCompletionException() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.CompletionException
+
+ // Simulate deeply nested wrapping
+ async caller() {
+ def original = new java.io.FileNotFoundException("config.yml")
+ def wrapped = new CompletionException(new java.util.concurrent.ExecutionException(original))
+ def future = new CompletableFuture()
+ future.completeExceptionally(wrapped)
+ try {
+ await future
+ assert false
+ } catch (java.io.FileNotFoundException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "config.yml"
+ '''
+ }
+
+ @Test
+ void testDeepUnwrapExecutionException() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.CompletionException
+ import java.util.concurrent.ExecutionException
+
+ async caller() {
+ def original = new IllegalStateException("deep cause")
+ // Triple nesting: CompletionException -> ExecutionException -> CompletionException -> actual
+ def inner = new CompletionException(original)
+ def middle = new ExecutionException(inner)
+ def outer = new CompletionException(middle)
+ def future = new CompletableFuture()
+ future.completeExceptionally(outer)
+ try {
+ await future
+ assert false
+ } catch (IllegalStateException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "deep cause"
+ '''
+ }
+
+ // =========================================================================
+ // 3. Cancellation Support
+ // =========================================================================
+
+ @Test
+ void testCancelAwaitable() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CancellationException
+
+ async slowTask() {
+ Thread.sleep(10_000)
+ return 42
+ }
+
+ def task = slowTask()
+ assert !task.isCancelled()
+ assert !task.isDone()
+
+ boolean cancelled = task.cancel()
+ assert cancelled || task.isDone()
+
+ assert task.isCancelled()
+ assert task.isDone()
+ assert task.isCompletedExceptionally()
+
+ try {
+ await(task)
+ assert false : "should throw CancellationException"
+ } catch (CancellationException e) {
+ // expected — matches C#/Kotlin cancellation behavior
+ }
+ '''
+ }
+
+ @Test
+ void testCancelAlreadyCompletedIsNoOp() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def result = Awaitable.of(42)
+ assert !result.cancel()
+ assert !result.isCancelled()
+ assert result.isDone()
+ assert !result.isCompletedExceptionally()
+ assert await(result) == 42
+ '''
+ }
+
+ @Test
+ void testIsCompletedExceptionallyOnFailure() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ def failed = Awaitable.failed(new RuntimeException("boom"))
+ assert failed.isDone()
+ assert failed.isCompletedExceptionally()
+ assert !failed.isCancelled()
+ '''
+ }
+
+ // =========================================================================
+ // 4. awaitAll with mixed success/failure
+ // =========================================================================
+
+ @Test
+ void testAwaitAllWithFailureThrowsOriginal() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async success() { return 1 }
+
+ async failure() { throw new java.io.IOException("network error") }
+
+ async caller() {
+ try {
+ awaitAll(success(), failure())
+ assert false
+ } catch (java.io.IOException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "network error"
+ '''
+ }
+
+ @Test
+ void testAwaitAllWithAwaitableObjects() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async a() { return 10 }
+
+ async b() { return 20 }
+
+ async caller() {
+ def results = awaitAll(a(), b(), Awaitable.of(30))
+ return results
+ }
+
+ def r = await(caller())
+ assert r == [10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testAwaitAnyWithFailure() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ try {
+ awaitAny(Awaitable.failed(new java.io.IOException("fail")))
+ assert false
+ } catch (java.io.IOException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "fail"
+ '''
+ }
+
+ @Test
+ void testAwaitAnyWithMixedSourceTypes() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ async caller() {
+ def cf = CompletableFuture.completedFuture(42)
+ def aw = Awaitable.of(99)
+ def result = awaitAny(cf, aw)
+ return result
+ }
+
+ def r = await(caller())
+ assert r == 42 || r == 99
+ '''
+ }
+
+ // =========================================================================
+ // 5. awaitAllSettled — Promise.allSettled() equivalent
+ // =========================================================================
+
+ @Test
+ void testAwaitAllSettledMixedResults() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ def a = Awaitable.of(1)
+ def b = Awaitable.failed(new java.io.IOException("err"))
+ def c = Awaitable.of(3)
+ return awaitAllSettled(a, b, c)
+ }
+
+ def results = await(caller())
+ assert results.size() == 3
+
+ assert results[0].isSuccess()
+ assert results[0].value == 1
+
+ assert results[1].isFailure()
+ assert results[1].error instanceof java.io.IOException
+ assert results[1].error.message == "err"
+
+ assert results[2].isSuccess()
+ assert results[2].value == 3
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettledAllSuccess() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ return awaitAllSettled(
+ Awaitable.of("a"), Awaitable.of("b"), Awaitable.of("c")
+ )
+ }
+
+ def results = await(caller())
+ assert results.every { it.isSuccess() }
+ assert results*.value == ["a", "b", "c"]
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettledAllFailure() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ return awaitAllSettled(
+ Awaitable.failed(new RuntimeException("e1")),
+ Awaitable.failed(new java.io.IOException("e2")),
+ Awaitable.failed(new IllegalStateException("e3"))
+ )
+ }
+
+ def results = await(caller())
+ assert results.every { it.isFailure() }
+ assert results[0].error instanceof RuntimeException
+ assert results[1].error instanceof java.io.IOException
+ assert results[2].error instanceof IllegalStateException
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettledGetOrElse() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ def results = awaitAllSettled(
+ Awaitable.of(10),
+ Awaitable.failed(new RuntimeException("oops"))
+ )
+ return results.collect { it.getOrElse { err -> -1 } }
+ }
+
+ assert await(caller()) == [10, -1]
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettledNeverThrows() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ // Should NOT throw even though all fail
+ def results = awaitAllSettled(
+ Awaitable.failed(new Error("err1")),
+ Awaitable.failed(new RuntimeException("err2"))
+ )
+ return results.size()
+ }
+
+ assert await(caller()) == 2
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettledWithCancelledTask() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CancellationException
+ import java.util.concurrent.CompletableFuture
+
+ async caller() {
+ def cf = new CompletableFuture()
+ cf.cancel(true)
+ def results = awaitAllSettled(
+ Awaitable.of(1), cf
+ )
+ assert results[0].isSuccess()
+ assert results[0].value == 1
+ assert results[1].isFailure()
+ assert results[1].error instanceof CancellationException
+ return "ok"
+ }
+
+ assert await(caller()) == "ok"
+ '''
+ }
+
+ // =========================================================================
+ // 6. Generator (yield return) exception propagation
+ // =========================================================================
+
+ @Test
+ void testGeneratorCheckedExceptionPropagation() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async failingGenerator() {
+ yield return 1
+ yield return 2
+ throw new java.io.IOException("generator IO error")
+ }
+
+ async caller() {
+ def collected = []
+ try {
+ for await (item in failingGenerator()) {
+ collected << item
+ }
+ assert false : "should not complete normally"
+ } catch (java.io.IOException e) {
+ return "${collected}:${e.message}"
+ }
+ }
+
+ assert await(caller()) == "[1, 2]:generator IO error"
+ '''
+ }
+
+ @Test
+ void testGeneratorRuntimeExceptionPropagation() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async failingGenerator() {
+ yield return "a"
+ throw new IllegalArgumentException("bad value")
+ }
+
+ async caller() {
+ def items = []
+ try {
+ for await (item in failingGenerator()) {
+ items << item
+ }
+ assert false
+ } catch (IllegalArgumentException e) {
+ return "${items}:${e.message}"
+ }
+ }
+
+ assert await(caller()) == "[a]:bad value"
+ '''
+ }
+
+ // =========================================================================
+ // 7. try/catch/finally in async methods
+ // =========================================================================
+
+ @Test
+ void testTryCatchFinallyInAsync() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async riskyOp() {
+ throw new java.io.IOException("test")
+ }
+
+ async caller() {
+ def log = []
+ try {
+ log << "try"
+ await riskyOp()
+ log << "after-await"
+ } catch (java.io.IOException e) {
+ log << "catch:${e.message}"
+ } finally {
+ log << "finally"
+ }
+ return log.join(",")
+ }
+
+ assert await(caller()) == "try,catch:test,finally"
+ '''
+ }
+
+ @Test
+ void testMultipleCatchBlocks() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async throwIO() { throw new java.io.IOException("io") }
+
+ async throwArg() { throw new IllegalArgumentException("arg") }
+
+ async caller(boolean useIO) {
+ try {
+ if (useIO) {
+ await throwIO()
+ } else {
+ await throwArg()
+ }
+ return "none"
+ } catch (java.io.IOException e) {
+ return "io:${e.message}"
+ } catch (IllegalArgumentException e) {
+ return "arg:${e.message}"
+ }
+ }
+
+ assert await(caller(true)) == "io:io"
+ assert await(caller(false)) == "arg:arg"
+ '''
+ }
+
+ @Test
+ void testNestedTryCatch() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async fail1() { throw new java.io.IOException("first") }
+
+ async fail2() { throw new IllegalStateException("second") }
+
+ async caller() {
+ def results = []
+ try {
+ try {
+ await fail1()
+ } catch (java.io.IOException e) {
+ results << "inner:${e.message}"
+ }
+ await fail2()
+ } catch (IllegalStateException e) {
+ results << "outer:${e.message}"
+ }
+ return results.join(",")
+ }
+
+ assert await(caller()) == "inner:first,outer:second"
+ '''
+ }
+
+ @Test
+ void testFinallyRunsOnSuccess() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ def log = []
+ try {
+ def v = await Awaitable.of(42)
+ log << "value:$v"
+ } finally {
+ log << "finally"
+ }
+ return log.join(",")
+ }
+
+ assert await(caller()) == "value:42,finally"
+ '''
+ }
+
+ // =========================================================================
+ // 8. Exception chaining across multiple awaits
+ // =========================================================================
+
+ @Test
+ void testExceptionChainingThreeDeep() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async level3() {
+ throw new java.io.IOException("root cause")
+ }
+
+ async level2() {
+ return await level3()
+ }
+
+ async level1() {
+ return await level2()
+ }
+
+ async caller() {
+ try {
+ await level1()
+ assert false
+ } catch (java.io.IOException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "root cause"
+ '''
+ }
+
+ @Test
+ void testExceptionTransformAcrossMultipleAwaits() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async step1() {
+ throw new java.io.IOException("step1 failed")
+ }
+
+ async step2() {
+ try {
+ await step1()
+ } catch (java.io.IOException e) {
+ throw new IllegalStateException("step2 wraps: " + e.message, e)
+ }
+ }
+
+ async caller() {
+ try {
+ await step2()
+ assert false
+ } catch (IllegalStateException e) {
+ assert e.cause instanceof java.io.IOException
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "step2 wraps: step1 failed"
+ '''
+ }
+
+ // =========================================================================
+ // 9. Awaitable.exceptionally — error recovery
+ // =========================================================================
+
+ @Test
+ void testExceptionallyRecovery() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async failing() {
+ throw new java.io.IOException("network error")
+ }
+
+ async caller() {
+ def task = failing().exceptionally { err ->
+ assert err instanceof java.io.IOException
+ return "recovered: ${err.message}"
+ }
+ return await task
+ }
+
+ assert await(caller()) == "recovered: network error"
+ '''
+ }
+
+ @Test
+ void testExceptionallySeesUnwrappedCause() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.CompletionException
+
+ async caller() {
+ // Create a future that wraps a checked exception
+ def cf = new CompletableFuture()
+ cf.completeExceptionally(
+ new CompletionException(new java.io.IOException("wrapped"))
+ )
+ def task = new groovy.concurrent.GroovyPromise(cf)
+ def recovered = task.exceptionally { err ->
+ // Should receive the IOException, not CompletionException
+ return err.class.simpleName + ":" + err.message
+ }
+ return await recovered
+ }
+
+ assert await(caller()) == "IOException:wrapped"
+ '''
+ }
+
+ // =========================================================================
+ // 10. Awaitable.then — exception propagation
+ // =========================================================================
+
+ @Test
+ void testThenDoesNotRunOnFailure() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ def task = Awaitable.failed(new RuntimeException("boom"))
+ def mapped = task.then { v -> v * 2 }
+ try {
+ await mapped
+ assert false
+ } catch (RuntimeException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "boom"
+ '''
+ }
+
+ // =========================================================================
+ // 11. Direct await syntax with try/catch — no annotations
+ // =========================================================================
+
+ @Test
+ void testDirectAsyncClosureExceptionHandling() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ async caller() {
+ def task = async {
+ throw new java.io.IOException("from closure")
+ }
+ try {
+ await task
+ assert false
+ } catch (java.io.IOException e) {
+ return e.message
+ }
+ }
+
+ assert groovy.transform.AsyncUtils.await(caller()) == "from closure"
+ '''
+ }
+
+ // =========================================================================
+ // 12. AwaitResult API edge cases
+ // =========================================================================
+
+ @Test
+ void testAwaitResultAccessors() {
+ assertScript '''
+ import groovy.concurrent.AwaitResult
+
+ def s = AwaitResult.success(42)
+ assert s.isSuccess()
+ assert !s.isFailure()
+ assert s.value == 42
+ try {
+ s.error
+ assert false
+ } catch (IllegalStateException e) {
+ // expected
+ }
+
+ def f = AwaitResult.failure(new RuntimeException("err"))
+ assert f.isFailure()
+ assert !f.isSuccess()
+ assert f.error.message == "err"
+ try {
+ f.value
+ assert false
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ '''
+ }
+
+ @Test
+ void testAwaitResultToString() {
+ assertScript '''
+ import groovy.concurrent.AwaitResult
+
+ def s = AwaitResult.success("hello")
+ assert s.toString() == "AwaitResult.Success[hello]"
+
+ def f = AwaitResult.failure(new RuntimeException("boom"))
+ assert f.toString().startsWith("AwaitResult.Failure[")
+ '''
+ }
+
+ // =========================================================================
+ // 13. InterruptedException → CancellationException
+ // =========================================================================
+
+ @Test
+ void testInterruptedAwaitThrowsCancellation() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CancellationException
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ def task = Awaitable.of(42)
+ // Interrupt current thread before await
+ Thread.currentThread().interrupt()
+ try {
+ await task
+ // Awaitable.of is already complete, so get() may succeed
+ // (depending on impl). Clear interrupt flag.
+ Thread.interrupted()
+ return "completed"
+ } catch (CancellationException e) {
+ Thread.interrupted() // clear flag
+ return "cancelled"
+ }
+ }
+
+ def result = await(caller())
+ assert result == "completed" || result == "cancelled"
+ '''
+ }
+
+ // =========================================================================
+ // 14. Exception in for-await loop body
+ // =========================================================================
+
+ @Test
+ void testExceptionInForAwaitBody() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async generate() {
+ yield return 1
+ yield return 2
+ yield return 3
+ }
+
+ async caller() {
+ def collected = []
+ try {
+ for await (item in generate()) {
+ collected << item
+ if (item == 2) throw new IllegalStateException("stop at 2")
+ }
+ assert false
+ } catch (IllegalStateException e) {
+ return "${collected}:${e.message}"
+ }
+ }
+
+ assert await(caller()) == "[1, 2]:stop at 2"
+ '''
+ }
+
+ // =========================================================================
+ // 15. Async method returning failed Awaitable via exceptionally
+ // =========================================================================
+
+ @Test
+ void testRecoverAndContinue() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async fetchWithRetry() {
+ def attempt1 = Awaitable.failed(new java.io.IOException("timeout"))
+ def result1 = attempt1.exceptionally { err -> "fallback" }
+ return await result1
+ }
+
+ assert await(fetchWithRetry()) == "fallback"
+ '''
+ }
+
+ // =========================================================================
+ // 16. deepUnwrap exposed via AsyncUtils (package-private, test via reflection or direct)
+ // =========================================================================
+
+ @Test
+ void testDeepUnwrapUtility() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletionException
+ import java.util.concurrent.ExecutionException
+
+ def original = new java.io.IOException("root")
+ def l1 = new ExecutionException(original)
+ def l2 = new CompletionException(l1)
+ def l3 = new CompletionException(l2)
+
+ // Access package-private method via Groovy's meta
+ def unwrapped = deepUnwrap(l3)
+ assert unwrapped.is(original)
+ '''
+ }
+
+ @Test
+ void testDeepUnwrapStopsAtNonWrapper() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def ex = new IllegalArgumentException("not wrapped")
+ assert deepUnwrap(ex).is(ex)
+ '''
+ }
+
+ @Test
+ void testDeepUnwrapHandlesNullCause() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletionException
+
+ // CompletionException with null cause
+ def ce = new CompletionException((Throwable) null)
+ def result = deepUnwrap(ce)
+ assert result.is(ce)
+ '''
+ }
+
+ // =========================================================================
+ // 17. awaitAllSettled with CompletableFuture mix
+ // =========================================================================
+
+ @Test
+ void testAwaitAllSettledWithCompletableFutures() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletableFuture
+
+ async caller() {
+ def cf1 = CompletableFuture.completedFuture("ok")
+ def cf2 = CompletableFuture.failedFuture(new RuntimeException("bad"))
+ def results = awaitAllSettled(cf1, cf2)
+
+ assert results[0].isSuccess()
+ assert results[0].value == "ok"
+ assert results[1].isFailure()
+ assert results[1].error instanceof RuntimeException
+ return "verified"
+ }
+
+ assert await(caller()) == "verified"
+ '''
+ }
+
+ // =========================================================================
+ // 18. Exception with null message
+ // =========================================================================
+
+ @Test
+ void testExceptionWithNullMessage() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ async throwNull() {
+ throw new RuntimeException((String) null)
+ }
+
+ async caller() {
+ try {
+ await throwNull()
+ assert false
+ } catch (RuntimeException e) {
+ assert e.message == null
+ return "caught-null-msg"
+ }
+ }
+
+ assert await(caller()) == "caught-null-msg"
+ '''
+ }
+
+ // =========================================================================
+ // 19. Multiple sequential awaits with different exception types
+ // =========================================================================
+
+ @Test
+ void testSequentialAwaitsWithDifferentExceptions() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ def log = []
+
+ try {
+ await Awaitable.failed(new java.io.IOException("io"))
+ } catch (java.io.IOException e) {
+ log << "io:${e.message}"
+ }
+
+ try {
+ await Awaitable.failed(new NumberFormatException("nfe"))
+ } catch (NumberFormatException e) {
+ log << "nfe:${e.message}"
+ }
+
+ try {
+ await Awaitable.failed(new java.sql.SQLException("sql"))
+ } catch (java.sql.SQLException e) {
+ log << "sql:${e.message}"
+ }
+
+ return log.join(";")
+ }
+
+ assert await(caller()) == "io:io;nfe:nfe;sql:sql"
+ '''
+ }
+
+ // =========================================================================
+ // 20. Awaitable.failed + await throws original
+ // =========================================================================
+
+ @Test
+ void testAwaitOnFailedAwaitable() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ async caller() {
+ try {
+ await Awaitable.failed(new java.io.IOException("pre-failed"))
+ assert false
+ } catch (java.io.IOException e) {
+ return e.message
+ }
+ }
+
+ assert await(caller()) == "pre-failed"
+ '''
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncFrameworkIntegrationTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncFrameworkIntegrationTest.groovy
new file mode 100644
index 0000000000..9dd2824919
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncFrameworkIntegrationTest.groovy
@@ -0,0 +1,506 @@
+/*
+ * 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.concurrent.AwaitableAdapter
+import groovy.concurrent.AwaitableAdapterRegistry
+import groovy.concurrent.GroovyPromise
+import static groovy.transform.AsyncUtils.*
+import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.core.Maybe
+import io.reactivex.rxjava3.core.Observable
+import io.reactivex.rxjava3.core.Single
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionStage
+
+/**
+ * Integration tests for Groovy async/await with third-party reactive
+ * frameworks: Reactor (Spring WebFlux foundation), RxJava 3, and Spring-style
+ * async patterns.
+ * <p>
+ * These tests demonstrate that the {@link AwaitableAdapter} SPI allows
+ * transparent integration without coupling Groovy's core to any specific
+ * reactive library.
+ */
+class AsyncFrameworkIntegrationTest {
+
+ private ReactorAwaitableAdapter reactorAdapter
+ private RxJavaAwaitableAdapter rxJavaAdapter
+
+ @BeforeEach
+ void registerAdapters() {
+ reactorAdapter = new ReactorAwaitableAdapter()
+ rxJavaAdapter = new RxJavaAwaitableAdapter()
+ AwaitableAdapterRegistry.register(reactorAdapter)
+ AwaitableAdapterRegistry.register(rxJavaAdapter)
+ }
+
+ @AfterEach
+ void resetExecutor() {
+ setExecutor(null)
+ }
+
+ // =====================================================================
+ // Reactor (Spring WebFlux foundation) tests
+ // =====================================================================
+
+ @Test
+ void testAwaitReactorMono() {
+ def mono = Mono.just("reactor-value")
+ def result = await(mono)
+ assert result == "reactor-value"
+ }
+
+ @Test
+ void testAwaitReactorMonoWithMap() {
+ def mono = Mono.just(10).map { it * 3 }
+ def result = await(mono)
+ assert result == 30
+ }
+
+ @Test
+ void testAwaitReactorMonoEmpty() {
+ def mono = Mono.empty()
+ def result = await(mono)
+ assert result == null
+ }
+
+ @Test
+ void testAwaitReactorMonoError() {
+ def mono = Mono.error(new IllegalStateException("reactor error"))
+ try {
+ await(mono)
+ assert false : "should have thrown"
+ } catch (IllegalStateException e) {
+ assert e.message == "reactor error"
+ }
+ }
+
+ @Test
+ void testAwaitReactorMonoDeferred() {
+ def mono = Mono.fromSupplier { Thread.sleep(50); "deferred" }
+ def result = await(mono)
+ assert result == "deferred"
+ }
+
+ @Test
+ void testAwaitReactorMonoChain() {
+ def mono = Mono.just("hello")
+ .map { it.toUpperCase() }
+ .flatMap { s -> Mono.just("${s}!") }
+ def result = await(mono)
+ assert result == "HELLO!"
+ }
+
+ @Test
+ void testForAwaitReactorFlux() {
+ def flux = Flux.just(1, 2, 3, 4, 5)
+ AsyncStream<Integer> stream = toAsyncStream(flux)
+ def results = []
+ while (await(stream.moveNext())) {
+ results << stream.getCurrent()
+ }
+ assert results == [1, 2, 3, 4, 5]
+ }
+
+ @Test
+ void testForAwaitReactorFluxWithOperators() {
+ def flux = Flux.range(1, 10).filter { it % 2 == 0 }.map { it * 10 }
+ AsyncStream<Integer> stream = toAsyncStream(flux)
+ def results = []
+ while (await(stream.moveNext())) {
+ results << stream.getCurrent()
+ }
+ assert results == [20, 40, 60, 80, 100]
+ }
+
+ @Test
+ void testForAwaitReactorFluxEmpty() {
+ def flux = Flux.empty()
+ AsyncStream stream = toAsyncStream(flux)
+ def results = []
+ while (await(stream.moveNext())) {
+ results << stream.getCurrent()
+ }
+ assert results == []
+ }
+
+ @Test
+ void testReactorMonoToAwaitableApi() {
+ def mono = Mono.just(42)
+ Awaitable<Integer> awaitable = AwaitableAdapterRegistry.toAwaitable(mono)
+ assert awaitable.get() == 42
+ assert awaitable.isDone()
+ }
+
+ @Test
+ void testReactorMonoAwaitableThen() {
+ def mono = Mono.just(5)
+ Awaitable<Integer> a = AwaitableAdapterRegistry.toAwaitable(mono)
+ Awaitable<Integer> doubled = a.then { it * 2 }
+ assert doubled.get() == 10
+ }
+
+ // =====================================================================
+ // RxJava 3 tests
+ // =====================================================================
+
+ @Test
+ void testAwaitRxJavaSingle() {
+ def single = Single.just("rxjava-value")
+ def result = await(single)
+ assert result == "rxjava-value"
+ }
+
+ @Test
+ void testAwaitRxJavaSingleWithMap() {
+ def single = Single.just(7).map { it * 6 }
+ def result = await(single)
+ assert result == 42
+ }
+
+ @Test
+ void testAwaitRxJavaSingleError() {
+ def single = Single.error(new RuntimeException("rx error"))
+ try {
+ await(single)
+ assert false : "should have thrown"
+ } catch (RuntimeException e) {
+ assert e.message == "rx error"
+ }
+ }
+
+ @Test
+ void testAwaitRxJavaMaybe() {
+ def maybe = Maybe.just("maybe-value")
+ def result = await(maybe)
+ assert result == "maybe-value"
+ }
+
+ @Test
+ void testAwaitRxJavaMaybeEmpty() {
+ def maybe = Maybe.empty()
+ def result = await(maybe)
+ assert result == null
+ }
+
+ @Test
+ void testForAwaitRxJavaObservable() {
+ def observable = Observable.just("a", "b", "c")
+ AsyncStream<String> stream = toAsyncStream(observable)
+ def results = []
+ while (await(stream.moveNext())) {
+ results << stream.getCurrent()
+ }
+ assert results == ["a", "b", "c"]
+ }
+
+ @Test
+ void testForAwaitRxJavaObservableWithOperators() {
+ def observable = Observable.range(1, 5).filter { it > 2 }.map { "item-${it}" }
+ AsyncStream<String> stream = toAsyncStream(observable)
+ def results = []
+ while (await(stream.moveNext())) {
+ results << stream.getCurrent()
+ }
+ assert results == ["item-3", "item-4", "item-5"]
+ }
+
+ @Test
+ void testForAwaitRxJavaFlowable() {
+ def flowable = Flowable.just(10, 20, 30)
+ AsyncStream<Integer> stream = toAsyncStream(flowable)
+ def results = []
+ while (await(stream.moveNext())) {
+ results << stream.getCurrent()
+ }
+ assert results == [10, 20, 30]
+ }
+
+ @Test
+ void testRxJavaSingleToAwaitableApi() {
+ def single = Single.just("adapted")
+ Awaitable<String> awaitable = AwaitableAdapterRegistry.toAwaitable(single)
+ assert awaitable.get() == "adapted"
+ assert awaitable.isDone()
+ }
+
+ // =====================================================================
+ // Spring-style async pattern tests
+ //
+ // Spring @Async methods return CompletableFuture (Spring 6+) or
+ // CompletionStage. Spring WebFlux returns Reactor Mono/Flux.
+ // These tests demonstrate seamless interoperability.
+ // =====================================================================
+
+ @Test
+ void testSpringStyleCompletableFutureService() {
+ // Simulates a Spring @Async service method returning CompletableFuture
+ def springService = new SpringStyleService()
+ def result = await(springService.fetchUser(1L))
+ assert result == [id: 1L, name: "User1"]
+ }
+
+ @Test
+ void testSpringStyleCompletionStageService() {
+ // Simulates a Spring service returning CompletionStage
+ def springService = new SpringStyleService()
+ CompletionStage<String> stage = springService.processAsync("hello")
+ def result = await(stage)
+ assert result == "HELLO"
+ }
+
+ @Test
+ void testSpringWebFluxMonoEndpoint() {
+ // Simulates a Spring WebFlux reactive endpoint returning Mono
+ def controller = new SpringWebFluxStyleController()
+ def mono = controller.getUser(42L)
+ def result = await(mono)
+ assert result == [id: 42L, name: "User42"]
+ }
+
+ @Test
+ void testSpringWebFluxFluxEndpoint() {
+ // Simulates a Spring WebFlux reactive endpoint returning Flux
+ def controller = new SpringWebFluxStyleController()
+ def flux = controller.listUsers()
+ AsyncStream stream = toAsyncStream(flux)
+ def results = []
+ while (await(stream.moveNext())) {
+ results << stream.getCurrent()
+ }
+ assert results.size() == 3
+ assert results[0].name == "User1"
+ assert results[2].name == "User3"
+ }
+
+ @Test
+ void testSpringStyleAsyncChainWithReactorMono() {
+ // Simulates chaining Spring async calls with Reactor
+ def userService = new SpringWebFluxStyleController()
+ def enriched = userService.getUser(1L)
+ .map { user -> user + [role: "admin"] }
+ def result = await(enriched)
+ assert result.name == "User1"
+ assert result.role == "admin"
+ }
+
+ @Test
+ void testSpringStyleMultipleAsyncCalls() {
+ // Simulates parallel Spring async calls gathered with Groovy await
+ def service = new SpringStyleService()
+ def f1 = service.fetchUser(1L)
+ def f2 = service.fetchUser(2L)
+ def f3 = service.fetchUser(3L)
+ def results = [await(f1), await(f2), await(f3)]
+ assert results.collect { it.name } == ["User1", "User2", "User3"]
+ }
+
+ @Test
+ void testSpringStyleCompletionStageAdapter() {
+ // CompletionStage (Spring's return type) directly supported by built-in adapter
+ CompletionStage<String> stage = CompletableFuture.supplyAsync { "stage-value" }
+ Awaitable<String> awaitable = AwaitableAdapterRegistry.toAwaitable(stage)
+ assert awaitable.get() == "stage-value"
+ }
+
+ // =====================================================================
+ // Cross-framework interoperability
+ // =====================================================================
+
+ @Test
+ void testMixedFrameworkAwait() {
+ // Await values from different frameworks in the same context
+ def mono = Mono.just(10)
+ def single = Single.just(20)
+ def cf = CompletableFuture.supplyAsync { 30 }
+
+ def r1 = await(mono)
+ def r2 = await(single)
+ def r3 = await(cf)
+ assert r1 + r2 + r3 == 60
+ }
+
+ @Test
+ void testReactorToRxJavaInterop() {
+ // Convert between frameworks via Awaitable abstraction
+ def mono = Mono.just("from-reactor")
+ Awaitable<String> awaitable = AwaitableAdapterRegistry.toAwaitable(mono)
+
+ // Use Awaitable's CompletableFuture interop to bridge to any framework
+ CompletableFuture<String> cf = awaitable.toCompletableFuture()
+ assert cf.get() == "from-reactor"
+ }
+
+ @Test
+ void testAwaitableExceptionallyWithReactor() {
+ def mono = Mono.error(new RuntimeException("fail"))
+ Awaitable<String> awaitable = AwaitableAdapterRegistry.toAwaitable(mono)
+ Awaitable<String> recovered = awaitable.exceptionally { "recovered" }
+ assert recovered.get() == "recovered"
+ }
+
+ @Test
+ void testAwaitableThenComposeAcrossFrameworks() {
+ def mono = Mono.just(5)
+ Awaitable<Integer> a = AwaitableAdapterRegistry.toAwaitable(mono)
+ Awaitable<Integer> composed = a.thenCompose { val ->
+ // Chain into another Awaitable from a different source
+ GroovyPromise.of(CompletableFuture.supplyAsync { val * 10 })
+ }
+ assert composed.get() == 50
+ }
+
+ // =====================================================================
+ // Adapter implementation classes
+ // =====================================================================
+
+ /**
+ * Reactor adapter: supports {@link Mono} as {@link Awaitable} and
+ * {@link Flux} as {@link AsyncStream}. This is the pattern Spring
+ * WebFlux users would register.
+ */
+ static class ReactorAwaitableAdapter implements AwaitableAdapter {
+
+ @Override
+ boolean supportsAwaitable(Class<?> type) {
+ return Mono.isAssignableFrom(type)
+ }
+
+ @Override
+ Awaitable toAwaitable(Object source) {
+ return new GroovyPromise<>(((Mono) source).toFuture())
+ }
+
+ @Override
+ boolean supportsAsyncStream(Class<?> type) {
+ return Flux.isAssignableFrom(type)
+ }
+
+ @Override
+ AsyncStream toAsyncStream(Object source) {
+ def iter = ((Flux) source).toIterable().iterator()
+ return new AsyncStream() {
+ private Object current
+
+ @Override
+ Awaitable<Boolean> moveNext() {
+ boolean hasNext = iter.hasNext()
+ if (hasNext) current = iter.next()
+ return Awaitable.of(hasNext)
+ }
+
+ @Override
+ Object getCurrent() { return current }
+ }
+ }
+ }
+
+ /**
+ * RxJava 3 adapter: supports {@link Single}/{@link Maybe} as
+ * {@link Awaitable} and {@link Observable}/{@link Flowable} as
+ * {@link AsyncStream}.
+ */
+ static class RxJavaAwaitableAdapter implements AwaitableAdapter {
+
+ @Override
+ boolean supportsAwaitable(Class<?> type) {
+ return Single.isAssignableFrom(type) || Maybe.isAssignableFrom(type)
+ }
+
+ @Override
+ Awaitable toAwaitable(Object source) {
+ if (source instanceof Single) {
+ return new GroovyPromise<>(((Single) source).toCompletionStage().toCompletableFuture())
+ }
+ if (source instanceof Maybe) {
+ return new GroovyPromise<>(((Maybe) source).toCompletionStage(null).toCompletableFuture())
+ }
+ throw new IllegalArgumentException("Unsupported RxJava type: ${source.class}")
+ }
+
+ @Override
+ boolean supportsAsyncStream(Class<?> type) {
+ return Observable.isAssignableFrom(type) || Flowable.isAssignableFrom(type)
+ }
+
+ @Override
+ AsyncStream toAsyncStream(Object source) {
+ Iterator iter
+ if (source instanceof Observable) {
+ iter = ((Observable) source).blockingIterable().iterator()
+ } else if (source instanceof Flowable) {
+ iter = ((Flowable) source).blockingIterable().iterator()
+ } else {
+ throw new IllegalArgumentException("Unsupported RxJava type: ${source.class}")
+ }
+ return new AsyncStream() {
+ private Object current
+
+ @Override
+ Awaitable<Boolean> moveNext() {
+ boolean hasNext = iter.hasNext()
+ if (hasNext) current = iter.next()
+ return Awaitable.of(hasNext)
+ }
+
+ @Override
+ Object getCurrent() { return current }
+ }
+ }
+ }
+
+ // =====================================================================
+ // Spring-style service stubs (simulate Spring @Async / WebFlux)
+ // =====================================================================
+
+ /** Simulates a Spring {@code @Async} service returning CompletableFuture. */
+ static class SpringStyleService {
+ CompletableFuture<Map> fetchUser(long id) {
+ CompletableFuture.supplyAsync { [id: id, name: "User${id}"] }
+ }
+
+ CompletionStage<String> processAsync(String input) {
+ CompletableFuture.supplyAsync { input.toUpperCase() }
+ }
+ }
+
+ /** Simulates a Spring WebFlux controller returning Reactor types. */
+ static class SpringWebFluxStyleController {
+ Mono<Map> getUser(long id) {
+ Mono.just([id: id, name: "User${id}"])
+ }
+
+ Flux<Map> listUsers() {
+ Flux.just(
+ [id: 1L, name: "User1"],
+ [id: 2L, name: "User2"],
+ [id: 3L, name: "User3"]
+ )
+ }
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncTransformTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncTransformTest.groovy
new file mode 100644
index 0000000000..1e2ea547d6
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncTransformTest.groovy
@@ -0,0 +1,1401 @@
+/*
+ * 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.transform.Async
+import static groovy.transform.AsyncUtils.*
+import org.codehaus.groovy.control.CompilationFailedException
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.ForkJoinPool
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Tests for async/await: @Async annotation, async/await keywords,
+ * Awaitable abstraction, for-await, and AsyncUtils.
+ */
+class AsyncTransformTest {
+
+ @AfterEach
+ void resetExecutor() {
+ setExecutor(null)
+ }
+
+ // ==== @Async annotation tests ====
+
+ @Test
+ void testBasicAsyncReturnsAwaitable() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Service {
+ @Async
+ def fetchData() {
+ return "hello"
+ }
+ }
+
+ def service = new Service()
+ def future = service.fetchData()
+ assert future instanceof Awaitable
+ assert future.get() == "hello"
+ '''
+ }
+
+ @Test
+ void testAsyncWithTypedReturnValue() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Service {
+ @Async
+ String compute() {
+ return "typed-result"
+ }
+ }
+
+ def service = new Service()
+ Awaitable<String> future = service.compute()
+ assert future.get() == "typed-result"
+ '''
+ }
+
+ @Test
+ void testAsyncWithIntReturnValue() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Calculator {
+ @Async
+ int add(int a, int b) {
+ return a + b
+ }
+ }
+
+ def calc = new Calculator()
+ def future = calc.add(3, 4)
+ assert future instanceof Awaitable
+ assert future.get() == 7
+ '''
+ }
+
+ @Test
+ void testAsyncVoidMethod() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.atomic.AtomicBoolean
+
+ class Worker {
+ AtomicBoolean executed = new AtomicBoolean(false)
+
+ @Async
+ void doWork() {
+ executed.set(true)
+ }
+ }
+
+ def worker = new Worker()
+ def future = worker.doWork()
+ assert future instanceof Awaitable
+ future.get()
+ assert worker.executed.get()
+ '''
+ }
+
+ @Test
+ void testAsyncStaticMethod() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Util {
+ @Async
+ static String process(String input) {
+ return input.toUpperCase()
+ }
+ }
+
+ def future = Util.process("hello")
+ assert future instanceof Awaitable
+ assert future.get() == "HELLO"
+ '''
+ }
+
+ // ==== await() call-style tests (backward compat) ====
+
+ @Test
+ void testAwaitCallInAsyncMethod() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def fetchAndProcess() {
+ def value = await(CompletableFuture.supplyAsync { 42 })
+ return value * 2
+ }
+ }
+
+ def service = new Service()
+ assert service.fetchAndProcess().get() == 84
+ '''
+ }
+
+ @Test
+ void testMultipleSequentialAwaits() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def combineResults() {
+ def a = await(CompletableFuture.supplyAsync { 10 })
+ def b = await(CompletableFuture.supplyAsync { 20 })
+ def c = await(CompletableFuture.supplyAsync { 30 })
+ return a + b + c
+ }
+ }
+
+ def service = new Service()
+ assert service.combineResults().get() == 60
+ '''
+ }
+
+ @Test
+ void testAwaitWithDependentFutures() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def pipeline() {
+ def raw = await(CompletableFuture.supplyAsync { "hello world" })
+ def upper = await(CompletableFuture.supplyAsync { raw.toUpperCase() })
+ return upper
+ }
+ }
+
+ def service = new Service()
+ assert service.pipeline().get() == "HELLO WORLD"
+ '''
+ }
+
+ @Test
+ void testNestedAwaitInExpression() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def nestedExpr() {
+ return await(CompletableFuture.supplyAsync { 5 }) + await(CompletableFuture.supplyAsync { 3 })
+ }
+ }
+
+ def service = new Service()
+ assert service.nestedExpr().get() == 8
+ '''
+ }
+
+ @Test
+ void testAwaitInsideConditional() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def conditional(boolean flag) {
+ if (flag) {
+ return await(CompletableFuture.supplyAsync { "yes" })
+ } else {
+ return await(CompletableFuture.supplyAsync { "no" })
+ }
+ }
+ }
+
+ def service = new Service()
+ assert service.conditional(true).get() == "yes"
+ assert service.conditional(false).get() == "no"
+ '''
+ }
+
+ @Test
+ void testAwaitInsideLoop() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def sumAsync(int count) {
+ int sum = 0
+ for (int i = 1; i <= count; i++) {
+ sum += await(CompletableFuture.supplyAsync { i })
+ }
+ return sum
+ }
+ }
+
+ def service = new Service()
+ assert service.sumAsync(5).get() == 15
+ '''
+ }
+
+ @Test
+ void testAwaitInsideTryCatch() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def safeOperation() {
+ try {
+ def result = await(CompletableFuture.supplyAsync { 42 })
+ return result
+ } catch (Exception e) {
+ return -1
+ }
+ }
+ }
+
+ def service = new Service()
+ assert service.safeOperation().get() == 42
+ '''
+ }
+
+ @Test
+ void testAwaitInsideClosureInAsyncMethod() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def collectResults() {
+ def futures = [
+ CompletableFuture.supplyAsync { 1 },
+ CompletableFuture.supplyAsync { 2 },
+ CompletableFuture.supplyAsync { 3 }
+ ]
+ return futures.collect { f -> await(f) }
+ }
+ }
+
+ def service = new Service()
+ assert service.collectResults().get() == [1, 2, 3]
+ '''
+ }
+
+ // ==== Exception handling ====
+
+ @Test
+ void testAsyncExceptionPropagation() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.ExecutionException
+
+ class Service {
+ @Async
+ def failingMethod() {
+ throw new IllegalStateException("async failure")
+ }
+ }
+
+ def service = new Service()
+ def future = service.failingMethod()
+ try {
+ future.get()
+ assert false : "should have thrown"
+ } catch (ExecutionException e) {
+ assert e.cause instanceof IllegalStateException
+ assert e.cause.message == "async failure"
+ }
+ '''
+ }
+
+ @Test
+ void testAwaitExceptionFromFuture() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.ExecutionException
+
+ class Service {
+ @Async
+ def awaitFailing() {
+ return await(CompletableFuture.supplyAsync {
+ throw new IllegalArgumentException("inner failure")
+ })
+ }
+ }
+
+ def service = new Service()
+ try {
+ service.awaitFailing().get()
+ assert false : "should have thrown"
+ } catch (ExecutionException e) {
+ assert e.cause instanceof IllegalArgumentException
+ assert e.cause.message == "inner failure"
+ }
+ '''
+ }
+
+ @Test
+ void testAwaitExceptionCaughtInTryCatch() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def recoverFromError() {
+ try {
+ return await(CompletableFuture.supplyAsync {
+ throw new IllegalArgumentException("oops")
+ })
+ } catch (IllegalArgumentException e) {
+ return "recovered: " + e.message
+ }
+ }
+ }
+
+ def service = new Service()
+ assert service.recoverFromError().get() == "recovered: oops"
+ '''
+ }
+
+ // ==== Custom executor ====
+
+ @Test
+ void testAsyncWithCustomExecutor() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.Executors
+
+ class Service {
+ def myPool = Executors.newSingleThreadExecutor()
+
+ @Async(executor = "myPool")
+ def runOnCustomPool() {
+ return Thread.currentThread().name
+ }
+ }
+
+ def service = new Service()
+ def threadName = service.runOnCustomPool().get()
+ assert threadName.contains("pool")
+ service.myPool.shutdown()
+ '''
+ }
+
+ @Test
+ void testAsyncStaticMethodWithStaticExecutor() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.Executors
+
+ class Service {
+ static def pool = Executors.newSingleThreadExecutor()
+
+ @Async(executor = "pool")
+ static def runStatic() {
+ return "static-result"
+ }
+ }
+
+ assert Service.runStatic().get() == "static-result"
+ Service.pool.shutdown()
+ '''
+ }
+
+ // ==== Concurrency ====
+
+ @Test
+ void testAsyncMethodRunsOnDifferentThread() {
+ assertScript '''
+ import groovy.transform.Async
+
+ class Service {
+ @Async
+ def getThreadName() {
+ return Thread.currentThread().name
+ }
+ }
+
+ def service = new Service()
+ def callerThread = Thread.currentThread().name
+ def asyncThread = service.getThreadName().get()
+ assert asyncThread != callerThread || asyncThread.contains("ForkJoinPool")
+ '''
+ }
+
+ @Test
+ void testConcurrentAsyncExecution() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CountDownLatch
+ import java.util.concurrent.atomic.AtomicInteger
+
+ class Service {
+ AtomicInteger concurrentCount = new AtomicInteger(0)
+ AtomicInteger maxConcurrent = new AtomicInteger(0)
+
+ @Async
+ def concurrentTask(CountDownLatch startLatch) {
+ int current = concurrentCount.incrementAndGet()
+ maxConcurrent.updateAndGet { max -> Math.max(max, current) }
+ startLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)
+ concurrentCount.decrementAndGet()
+ return current
+ }
+ }
+
+ def service = new Service()
+ def latch = new CountDownLatch(1)
+ def futures = (1..10).collect { service.concurrentTask(latch) }
+ Thread.sleep(500)
+ latch.countDown()
+ futures.each { it.get(5, java.util.concurrent.TimeUnit.SECONDS) }
+ assert service.maxConcurrent.get() > 1
+ '''
+ }
+
+ @Test
+ void testHighConcurrencyStressTest() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.atomic.AtomicInteger
+
+ class Service {
+ AtomicInteger counter = new AtomicInteger(0)
+
+ @Async
+ def increment() {
+ counter.incrementAndGet()
+ return counter.get()
+ }
+ }
+
+ def service = new Service()
+ int taskCount = 1000
+ def futures = (1..taskCount).collect { service.increment() }
+ futures.each { it.get(10, java.util.concurrent.TimeUnit.SECONDS) }
+ assert service.counter.get() == taskCount
+ '''
+ }
+
+ @Test
+ void testParallelAwaitPattern() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ CompletableFuture<Integer> asyncCompute(int value) {
+ return CompletableFuture.supplyAsync {
+ Thread.sleep(50)
+ return value * 2
+ }
+ }
+
+ @Async
+ def parallelFetch() {
+ def f1 = asyncCompute(1)
+ def f2 = asyncCompute(2)
+ def f3 = asyncCompute(3)
+ return await(f1) + await(f2) + await(f3)
+ }
+ }
+
+ def service = new Service()
+ long start = System.currentTimeMillis()
+ def result = service.parallelFetch().get()
+ long elapsed = System.currentTimeMillis() - start
+ assert result == 12
+ assert elapsed < 500
+ '''
+ }
+
+ // ==== AsyncUtils standalone ====
+
+ @Test
+ void testAsyncUtilsAsyncClosure() {
+ def future = async { 42 }
+ assert future instanceof groovy.concurrent.Awaitable
+ assert future.get() == 42
+ }
+
+ @Test
+ void testAsyncUtilsAwait() {
+ def future = CompletableFuture.supplyAsync { "hello" }
+ def result = await(future)
+ assert result == "hello"
+ }
+
+ @Test
+ void testAsyncUtilsAwaitAll() {
+ def f1 = CompletableFuture.supplyAsync { 1 }
+ def f2 = CompletableFuture.supplyAsync { 2 }
+ def f3 = CompletableFuture.supplyAsync { 3 }
+ def results = awaitAll(f1, f2, f3)
+ assert results == [1, 2, 3]
+ }
+
+ @Test
+ void testAsyncUtilsAwaitAny() {
+ def f1 = CompletableFuture.supplyAsync { "first" }
+ def f2 = new CompletableFuture()
+ def f3 = new CompletableFuture()
+ def result = awaitAny(f1, f2, f3)
+ assert result == "first"
+ }
+
+ @Test
+ void testAsyncUtilsSetCustomExecutor() {
+ def pool = Executors.newSingleThreadExecutor()
+ try {
+ setExecutor(pool)
+ def future = async { Thread.currentThread().name }
+ assert future.get().contains("pool")
+ } finally {
+ setExecutor(null)
+ pool.shutdown()
+ }
+ }
+
+ @Test
+ void testAsyncUtilsAwaitFutureInterface() {
+ def pool = Executors.newSingleThreadExecutor()
+ try {
+ java.util.concurrent.Future<String> future = pool.submit({ "from-executor" } as java.util.concurrent.Callable)
+ def result = await(future)
+ assert result == "from-executor"
+ } finally {
+ pool.shutdown()
+ }
+ }
+
+ @Test
+ void testAsyncUtilsAwaitExceptionUnwrapping() {
+ def future = CompletableFuture.supplyAsync {
+ throw new IllegalStateException("test error")
+ }
+ try {
+ await(future)
+ assert false : "should have thrown"
+ } catch (IllegalStateException e) {
+ assert e.message == "test error"
+ }
+ }
+
+ // ==== Compilation errors ====
+
+ @Test
+ void testAsyncOnAbstractMethodFails() {
+ def err = shouldFail CompilationFailedException, '''
+ import groovy.transform.Async
+
+ abstract class Service {
+ @Async
+ abstract def fetchData()
+ }
+ '''
+ assert err.message.contains("cannot be applied to abstract method")
+ }
+
+ @Test
+ void testAsyncOnAwaitableReturnTypeFails() {
+ def err = shouldFail CompilationFailedException, '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Service {
+ @Async
+ Awaitable<String> fetchData() {
+ return groovy.concurrent.Awaitable.of("hello")
+ }
+ }
+ '''
+ assert err.message.contains("already returns an async type")
+ }
+
+ @Test
+ void testAsyncWithMissingExecutorFieldFails() {
+ def err = shouldFail CompilationFailedException, '''
+ import groovy.transform.Async
+
+ class Service {
+ @Async(executor = "nonExistentField")
+ def fetchData() {
+ return "hello"
+ }
+ }
+ '''
+ assert err.message.contains("executor field 'nonExistentField' not found")
+ }
+
+ @Test
+ void testAsyncStaticMethodWithInstanceExecutorFails() {
+ def err = shouldFail CompilationFailedException, '''
+ import groovy.transform.Async
+ import java.util.concurrent.Executors
+
+ class Service {
+ def myPool = Executors.newSingleThreadExecutor()
+
+ @Async(executor = "myPool")
+ static def fetchData() {
+ return "hello"
+ }
+ }
+ '''
+ assert err.message.contains("must be static for static method")
+ }
+
+ // ==== Static import async/await ====
+
+ @Test
+ void testAsyncClosureWithStaticImport() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletableFuture
+
+ def future = async {
+ def a = await(CompletableFuture.supplyAsync { 10 })
+ def b = await(CompletableFuture.supplyAsync { 20 })
+ return a + b
+ }
+ assert future.get() == 30
+ '''
+ }
+
+ @Test
+ void testAsyncClosureExceptionHandling() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.ExecutionException
+
+ def future = async {
+ throw new IllegalStateException("closure error")
+ }
+ try {
+ future.get()
+ assert false : "should have thrown"
+ } catch (ExecutionException e) {
+ assert e.cause instanceof IllegalStateException
+ assert e.cause.message == "closure error"
+ }
+ '''
+ }
+
+ // ==== Integration scenarios ====
+
+ @Test
+ void testAsyncProducerConsumerPattern() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Producer {
+ @Async
+ def produce(int id) {
+ Thread.sleep(10)
+ return "item-${id}"
+ }
+ }
+
+ class Consumer {
+ @Async
+ def consume(Awaitable<String> itemFuture) {
+ def item = await(itemFuture)
+ return "consumed: ${item}"
+ }
+ }
+
+ def producer = new Producer()
+ def consumer = new Consumer()
+ def item = producer.produce(1)
+ def result = consumer.consume(item)
+ assert result.get() == "consumed: item-1"
+ '''
+ }
+
+ @Test
+ void testAsyncChainedCalls() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ class Pipeline {
+ @Async
+ def step1() { return 1 }
+
+ @Async
+ def step2(Awaitable<Integer> input) {
+ return await(input) + 10
+ }
+
+ @Async
+ def step3(Awaitable<Integer> input) {
+ return await(input) * 2
+ }
+ }
+
+ def p = new Pipeline()
+ def r1 = p.step1()
+ def r2 = p.step2(r1)
+ def r3 = p.step3(r2)
+ assert r3.get() == 22
+ '''
+ }
+
+ @Test
+ void testAsyncWithMethodParameters() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def greet(String name, int times) {
+ def parts = []
+ for (int i = 0; i < times; i++) {
+ parts << await(CompletableFuture.supplyAsync { "Hello ${name}" })
+ }
+ return parts.join(", ")
+ }
+ }
+
+ def service = new Service()
+ assert service.greet("World", 3).get() == "Hello World, Hello World, Hello World"
+ '''
+ }
+
+ @Test
+ void testAsyncWithCollectionProcessing() {
+ assertScript '''
+ import groovy.transform.Async
+ import java.util.concurrent.CompletableFuture
+
+ class BatchProcessor {
+ @Async
+ def processBatch(List<Integer> items) {
+ def futures = items.collect { item ->
+ CompletableFuture.supplyAsync { item * item }
+ }
+ return futures.collect { f -> await(f) }
+ }
+ }
+
+ def processor = new BatchProcessor()
+ assert processor.processBatch([1, 2, 3, 4, 5]).get() == [1, 4, 9, 16, 25]
+ '''
+ }
+
+ @Test
+ void testAsyncAwaitAllPattern() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @Async
+ def fetchAll() {
+ def f1 = CompletableFuture.supplyAsync { "a" }
+ def f2 = CompletableFuture.supplyAsync { "b" }
+ def f3 = CompletableFuture.supplyAsync { "c" }
+ return awaitAll(f1, f2, f3)
+ }
+ }
+
+ def service = new Service()
+ assert service.fetchAll().get() == ["a", "b", "c"]
+ '''
+ }
+
+ @Test
+ void testAsyncInScript() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+
+ @Async
+ def asyncAdd(int a, int b) {
+ return a + b
+ }
+
+ def future = asyncAdd(3, 4)
+ assert future instanceof Awaitable
+ assert future.get() == 7
+ '''
+ }
+
+ // ==== 'async' keyword modifier ====
+
+ @Test
+ void testAsyncKeywordModifier() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Service {
+ async def fetchData() {
+ return "from-async-keyword"
+ }
+ }
+
+ def service = new Service()
+ def result = service.fetchData()
+ assert result instanceof Awaitable
+ assert result.get() == "from-async-keyword"
+ '''
+ }
+
+ @Test
+ void testAsyncKeywordWithAwaitKeyword() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ async def compute() {
+ def a = await CompletableFuture.supplyAsync { 10 }
+ def b = await CompletableFuture.supplyAsync { 20 }
+ return a + b
+ }
+ }
+
+ def service = new Service()
+ assert service.compute().get() == 30
+ '''
+ }
+
+ @Test
+ void testAsyncKeywordStaticMethod() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Util {
+ async static def process(String input) {
+ return input.toUpperCase()
+ }
+ }
+
+ def result = Util.process("hello")
+ assert result instanceof Awaitable
+ assert result.get() == "HELLO"
+ '''
+ }
+
+ @Test
+ void testAsyncKeywordVoidMethod() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.atomic.AtomicBoolean
+
+ class Worker {
+ AtomicBoolean done = new AtomicBoolean(false)
+
+ async void doWork() {
+ done.set(true)
+ }
+ }
+
+ def worker = new Worker()
+ def result = worker.doWork()
+ assert result instanceof Awaitable
+ result.get()
+ assert worker.done.get()
+ '''
+ }
+
+ @Test
+ void testAsyncKeywordInScript() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async def add(int a, int b) {
+ return a + b
+ }
+
+ def result = add(5, 7)
+ assert result instanceof Awaitable
+ assert result.get() == 12
+ '''
+ }
+
+ // ==== 'await' keyword expression ====
+
+ @Test
+ void testAwaitKeywordExpression() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ async def fetchAndDouble() {
+ def value = await CompletableFuture.supplyAsync { 21 }
+ return value * 2
+ }
+ }
+
+ def service = new Service()
+ assert service.fetchAndDouble().get() == 42
+ '''
+ }
+
+ @Test
+ void testAwaitKeywordPrecedence() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ async def addAwaits() {
+ // 'await' should bind to the immediate expression, not to the sum
+ // await a + await b => (await a) + (await b)
+ return (await CompletableFuture.supplyAsync { 10 }) + (await CompletableFuture.supplyAsync { 5 })
+ }
+ }
+
+ def service = new Service()
+ assert service.addAwaits().get() == 15
+ '''
+ }
+
+ @Test
+ void testAwaitKeywordWithParentheses() {
+ assertScript '''
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ @groovy.transform.Async
+ def compute() {
+ // await(expr) still works as await keyword + parenthesized expression
+ def a = await(CompletableFuture.supplyAsync { 100 })
+ return a
+ }
+ }
+
+ def service = new Service()
+ assert service.compute().get() == 100
+ '''
+ }
+
+ @Test
+ void testAwaitKeywordWithMethodCall() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ CompletableFuture<String> fetchRemote() {
+ CompletableFuture.supplyAsync { "remote-data" }
+ }
+
+ async def process() {
+ def data = await fetchRemote()
+ return "processed: ${data}"
+ }
+ }
+
+ def service = new Service()
+ assert service.process().get() == "processed: remote-data"
+ '''
+ }
+
+ @Test
+ void testAwaitKeywordInConditional() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class Service {
+ async def check(boolean flag) {
+ if (flag) {
+ return await CompletableFuture.supplyAsync { "yes" }
+ } else {
+ return await CompletableFuture.supplyAsync { "no" }
+ }
+ }
+ }
+
+ def service = new Service()
+ assert service.check(true).get() == "yes"
+ assert service.check(false).get() == "no"
+ '''
+ }
+
+ // ==== Awaitable abstraction tests ====
+
+ @Test
+ void testAwaitableInterface() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.GroovyPromise
+ import java.util.concurrent.CompletableFuture
+
+ // Test Awaitable.of()
+ def a = Awaitable.of("hello")
+ assert a.get() == "hello"
+ assert a.isDone()
+
+ // Test Awaitable.failed()
+ def f = Awaitable.failed(new RuntimeException("oops"))
+ try {
+ f.get()
+ assert false
+ } catch (java.util.concurrent.ExecutionException e) {
+ assert e.cause.message == "oops"
+ }
+ '''
+ }
+
+ @Test
+ void testGroovyPromiseThen() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.GroovyPromise
+ import java.util.concurrent.CompletableFuture
+
+ def p = GroovyPromise.of(CompletableFuture.supplyAsync { 10 })
+ def p2 = p.then { it * 3 }
+ assert p2.get() == 30
+ '''
+ }
+
+ @Test
+ void testGroovyPromiseThenCompose() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.GroovyPromise
+ import java.util.concurrent.CompletableFuture
+
+ def p = GroovyPromise.of(CompletableFuture.supplyAsync { 5 })
+ def p2 = p.thenCompose { val ->
+ GroovyPromise.of(CompletableFuture.supplyAsync { val * 4 })
+ }
+ assert p2.get() == 20
+ '''
+ }
+
+ @Test
+ void testGroovyPromiseExceptionally() {
+ assertScript '''
+ import groovy.concurrent.GroovyPromise
+ import java.util.concurrent.CompletableFuture
+
+ def p = GroovyPromise.of(CompletableFuture.supplyAsync { throw new RuntimeException("fail") })
+ def recovered = p.exceptionally { t -> "recovered" }
+ assert recovered.get() == "recovered"
+ '''
+ }
+
+ @Test
+ void testAwaitableToCompletableFuture() {
+ assertScript '''
+ import groovy.concurrent.GroovyPromise
+ import java.util.concurrent.CompletableFuture
+
+ def p = GroovyPromise.of(CompletableFuture.completedFuture("interop"))
+ CompletableFuture<String> cf = p.toCompletableFuture()
+ assert cf.get() == "interop"
+ '''
+ }
+
+ // ==== AwaitableAdapterRegistry tests ====
+
+ @Test
+ void testAdapterRegistryWithCompletableFuture() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.AwaitableAdapterRegistry
+ import java.util.concurrent.CompletableFuture
+
+ def cf = CompletableFuture.completedFuture("adapted")
+ Awaitable<String> a = AwaitableAdapterRegistry.toAwaitable(cf)
+ assert a.get() == "adapted"
+ '''
+ }
+
+ @Test
+ void testAdapterRegistryPassthroughAwaitable() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.AwaitableAdapterRegistry
+
+ def original = Awaitable.of(42)
+ def adapted = AwaitableAdapterRegistry.toAwaitable(original)
+ assert adapted.is(original)
+ '''
+ }
+
+ @Test
+ void testAdapterRegistryCustomAdapter() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.AwaitableAdapter
+ import groovy.concurrent.AwaitableAdapterRegistry
+ import groovy.concurrent.GroovyPromise
+ import java.util.concurrent.CompletableFuture
+
+ // Simulate a custom async type
+ class CustomPromise {
+ final String value
+ CustomPromise(String v) { this.value = v }
+ }
+
+ // Register custom adapter
+ AwaitableAdapterRegistry.register(new AwaitableAdapter() {
+ boolean supportsAwaitable(Class<?> type) { CustomPromise.isAssignableFrom(type) }
+ def <T> Awaitable<T> toAwaitable(Object source) {
+ return Awaitable.of(((CustomPromise) source).value)
+ }
+ })
+
+ def custom = new CustomPromise("custom-value")
+ Awaitable<String> a = AwaitableAdapterRegistry.toAwaitable(custom)
+ assert a.get() == "custom-value"
+ '''
+ }
+
+ @Test
+ void testAwaitWithCustomAdaptedType() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.AwaitableAdapter
+ import groovy.concurrent.AwaitableAdapterRegistry
+ import static groovy.transform.AsyncUtils.*
+
+ class Deferred {
+ final Object result
+ Deferred(Object r) { this.result = r }
+ }
+
+ AwaitableAdapterRegistry.register(new AwaitableAdapter() {
+ boolean supportsAwaitable(Class<?> type) { Deferred.isAssignableFrom(type) }
+ def <T> Awaitable<T> toAwaitable(Object source) {
+ return Awaitable.of(((Deferred) source).result)
+ }
+ })
+
+ // await works with custom types via adapter
+ def d = new Deferred("deferred-value")
+ def result = await(d)
+ assert result == "deferred-value"
+ '''
+ }
+
+ // ==== for-await tests ====
+
+ @Test
+ void testForAwaitWithList() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Processor {
+ @groovy.transform.Async
+ def processAll() {
+ def results = []
+ for await (item in [1, 2, 3, 4, 5]) {
+ results << item * 2
+ }
+ return results
+ }
+ }
+
+ def p = new Processor()
+ assert p.processAll().get() == [2, 4, 6, 8, 10]
+ '''
+ }
+
+ @Test
+ void testForAwaitWithBreak() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Processor {
+ @groovy.transform.Async
+ def findFirst() {
+ for await (item in [10, 20, 30, 40]) {
+ if (item > 20) {
+ return item
+ }
+ }
+ return -1
+ }
+ }
+
+ def p = new Processor()
+ assert p.findFirst().get() == 30
+ '''
+ }
+
+ @Test
+ void testForAwaitWithAsyncKeyword() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ class Worker {
+ async def sumItems() {
+ int sum = 0
+ for await (item in [1, 2, 3]) {
+ sum += item
+ }
+ return sum
+ }
+ }
+
+ def worker = new Worker()
+ assert worker.sumItems().get() == 6
+ '''
+ }
+
+ // ==== 'async' and 'await' as identifiers (backward compat) ====
+
+ @Test
+ void testAsyncAsVariableName() {
+ assertScript '''
+ def async = 42
+ assert async == 42
+ '''
+ }
+
+ @Test
+ void testAwaitAsVariableName() {
+ assertScript '''
+ def await = "hello"
+ assert await == "hello"
+ '''
+ }
+
+ @Test
+ void testAsyncAsMethodName() {
+ assertScript '''
+ class Compat {
+ def async() { return "compatible" }
+ }
+ assert new Compat().async() == "compatible"
+ '''
+ }
+
+ // ==== Combined async/await keyword with Awaitable ====
+
+ @Test
+ void testFullAsyncAwaitPipeline() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class DataService {
+ async def fetchUser(long id) {
+ return [name: "User${id}"]
+ }
+
+ async def fetchOrders(long id) {
+ return ["order1", "order2"]
+ }
+
+ async def getUserSummary(long id) {
+ def user = await fetchUser(id)
+ def orders = await fetchOrders(id)
+ return [user: user, orders: orders]
+ }
+ }
+
+ def service = new DataService()
+ def summary = service.getUserSummary(1).get()
+ assert summary.user.name == "User1"
+ assert summary.orders.size() == 2
+ '''
+ }
+
+ @Test
+ void testAsyncAwaitWithExceptionRecovery() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class ResilientService {
+ async def fetchWithFallback() {
+ try {
+ return await CompletableFuture.supplyAsync { throw new RuntimeException("primary failed") }
+ } catch (RuntimeException e) {
+ return "fallback-value"
+ }
+ }
+ }
+
+ def service = new ResilientService()
+ assert service.fetchWithFallback().get() == "fallback-value"
+ '''
+ }
+
+ @Test
+ void testAsyncAwaitMixedWithAnnotation() {
+ assertScript '''
+ import groovy.transform.Async
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ class MixedService {
+ // Annotation style
+ @Async
+ def annotationStyle() {
+ return await CompletableFuture.supplyAsync { "annotation" }
+ }
+
+ // Keyword style
+ async def keywordStyle() {
+ return await CompletableFuture.supplyAsync { "keyword" }
+ }
+
+ // They interoperate
+ async def combined() {
+ def a = await annotationStyle()
+ def b = await keywordStyle()
+ return "${a}+${b}"
+ }
+ }
+
+ def svc = new MixedService()
+ assert svc.combined().get() == "annotation+keyword"
+ '''
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy
new file mode 100644
index 0000000000..25930edd35
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy
@@ -0,0 +1,1115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Tests for virtual thread integration, executor configuration, exception
+ * handling consistency across async methods/closures/lambdas, and coverage
+ * improvements for the async/await feature.
+ *
+ * @since 6.0.0
+ */
+final class AsyncVirtualThreadTest {
+
+ // ---- Virtual thread detection and usage ----
+
+ @Test
+ void testVirtualThreadsAvailable() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ // JDK 21+ should have virtual threads
+ def jdkVersion = Runtime.version().feature()
+ if (jdkVersion >= 21) {
+ assert isVirtualThreadsAvailable()
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncMethodRunsOnVirtualThread() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class VTService {
+ async checkThread() {
+ return Thread.currentThread().isVirtual()
+ }
+ }
+
+ if (isVirtualThreadsAvailable()) {
+ def service = new VTService()
+ def result = service.checkThread().get()
+ assert result == true
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncClosureRunsOnVirtualThread() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ if (isVirtualThreadsAvailable()) {
+ def awaitable = async { Thread.currentThread().isVirtual() }
+ assert await(awaitable) == true
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncLambdaRunsOnVirtualThread() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ if (isVirtualThreadsAvailable()) {
+ def asyncFn = async { x -> Thread.currentThread().isVirtual() }
+ def result = await(asyncFn(42))
+ assert result == true
+ }
+ '''
+ }
+
+ @Test
+ void testForAwaitRunsOnVirtualThread() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.AsyncStream
+
+ class VTGenerator {
+ async generate() {
+ yield return Thread.currentThread().isVirtual()
+ }
+ }
+
+ if (isVirtualThreadsAvailable()) {
+ def gen = new VTGenerator()
+ def stream = gen.generate()
+ def results = []
+ for await (item in stream) {
+ results << item
+ }
+ assert results == [true]
+ }
+ '''
+ }
+
+ @Test
+ void testHighConcurrencyWithVirtualThreads() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class HighConcurrency {
+ async compute(int n) {
+ Thread.sleep(10)
+ return n * 2
+ }
+ }
+
+ if (isVirtualThreadsAvailable()) {
+ def svc = new HighConcurrency()
+ // Launch 1000 concurrent tasks — trivial with virtual threads
+ def awaitables = (1..1000).collect { svc.compute(it) }
+ def results = awaitAll(awaitables as Object[])
+ assert results.size() == 1000
+ assert results[0] == 2
+ assert results[999] == 2000
+ }
+ '''
+ }
+
+ // ---- Executor configuration ----
+
+ @Test
+ void testCustomExecutorOverride() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.Executors
+ import java.util.concurrent.atomic.AtomicReference
+
+ def savedExecutor = getExecutor()
+ try {
+ def customPool = Executors.newFixedThreadPool(2, { r ->
+ def t = new Thread(r)
+ t.setName("custom-async-" + t.getId())
+ t
+ })
+ setExecutor(customPool)
+
+ def awaitable = async {
+ Thread.currentThread().getName()
+ }
+ def threadName = await(awaitable)
+ assert threadName.startsWith("custom-async-")
+ } finally {
+ setExecutor(savedExecutor)
+ }
+ '''
+ }
+
+ @Test
+ void testSetExecutorNullResetsToDefault() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.Executors
+
+ def originalExecutor = getExecutor()
+ // Set a custom executor
+ setExecutor(Executors.newSingleThreadExecutor())
+ assert getExecutor() != originalExecutor
+ // Reset to null — should restore default
+ setExecutor(null)
+ def restored = getExecutor()
+ assert restored != null
+ // Verify it works
+ def awaitable = groovy.transform.AsyncUtils.async { 42 }
+ assert groovy.transform.AsyncUtils.await(awaitable) == 42
+ // Restore original
+ setExecutor(originalExecutor)
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithCustomExecutorField() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.Executor
+ import java.util.concurrent.Executors
+
+ class CustomExecutorService {
+ static Executor myPool = Executors.newFixedThreadPool(1, { r ->
+ def t = new Thread(r)
+ t.setName("my-pool-thread")
+ t
+ })
+
+ @Async(executor = "myPool")
+ def doWork() {
+ return Thread.currentThread().getName()
+ }
+ }
+
+ def svc = new CustomExecutorService()
+ def result = svc.doWork().get()
+ assert result.startsWith("my-pool-thread")
+ '''
+ }
+
+ // ---- Exception handling consistency: async method vs closure vs lambda ----
+
+ @Test
+ void testCheckedExceptionConsistencyAcrossAsyncMethods() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class ExcService {
+ async failWithChecked() {
+ throw new java.io.IOException("async method error")
+ }
+ }
+
+ def svc = new ExcService()
+ try {
+ await(svc.failWithChecked())
+ assert false : "Should have thrown"
+ } catch (java.io.IOException e) {
+ assert e.message == "async method error"
+ }
+ '''
+ }
+
+ @Test
+ void testCheckedExceptionConsistencyAcrossClosures() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def awaitable = async { throw new java.io.IOException("closure error") }
+ try {
+ await(awaitable)
+ assert false : "Should have thrown"
+ } catch (java.io.IOException e) {
+ assert e.message == "closure error"
+ }
+ '''
+ }
+
+ @Test
+ void testCheckedExceptionConsistencyAcrossLambdas() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def asyncFn = async { x -> throw new java.io.IOException("lambda error: ${x}") }
+ try {
+ await(asyncFn("test"))
+ assert false : "Should have thrown"
+ } catch (java.io.IOException e) {
+ assert e.message == "lambda error: test"
+ }
+ '''
+ }
+
+ @Test
+ void testRuntimeExceptionConsistencyAllForms() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ // Form 1: async method
+ class Svc {
+ async failMethod() { throw new IllegalStateException("from method") }
+ }
+
+ // Form 2: async closure
+ def closure = async { throw new IllegalArgumentException("from closure") }
+
+ // Form 3: async lambda with params
+ def lambda = async { x -> throw new UnsupportedOperationException("from lambda") }
+
+ // All should throw the exact exception type (no wrapping)
+ try { await(new Svc().failMethod()); assert false }
+ catch (IllegalStateException e) { assert e.message == "from method" }
+
+ try { await(closure); assert false }
+ catch (IllegalArgumentException e) { assert e.message == "from closure" }
+
+ try { await(lambda("x")); assert false }
+ catch (UnsupportedOperationException e) { assert e.message == "from lambda" }
+ '''
+ }
+
+ @Test
+ void testErrorPropagationConsistencyAllForms() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class ErrorSvc {
+ async fail() { throw new StackOverflowError("method") }
+ }
+
+ // async method
+ try { await(new ErrorSvc().fail()); assert false }
+ catch (StackOverflowError e) { assert e.message == "method" }
+
+ // async closure
+ try { await(async { throw new StackOverflowError("closure") }); assert false }
+ catch (StackOverflowError e) { assert e.message == "closure" }
+
+ // async lambda
+ def fn = async { x -> throw new StackOverflowError("lambda") }
+ try { await(fn(1)); assert false }
+ catch (StackOverflowError e) { assert e.message == "lambda" }
+ '''
+ }
+
+ // ---- Async stream (yield return) consistency ----
+
+ @Test
+ void testYieldReturnConsistencyMethodVsClosure() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ // Form 1: async method with yield return
+ class GenSvc {
+ async range(int n) {
+ for (int i = 1; i <= n; i++) {
+ yield return i
+ }
+ }
+ }
+
+ // Form 2: async closure with yield return
+ def closureGen = async { for (int i = 1; i <= 3; i++) { yield return i * 10 } }
+
+ // Form 3: async lambda with yield return + params
+ def lambdaGen = async { n -> for (int i = 1; i <= n; i++) { yield return i * 100 } }
+
+ // Verify all produce correct streams
+ def methodResults = []
+ for await (item in new GenSvc().range(3)) { methodResults << item }
+ assert methodResults == [1, 2, 3]
+
+ def closureResults = []
+ for await (item in closureGen) { closureResults << item }
+ assert closureResults == [10, 20, 30]
+
+ def lambdaResults = []
+ for await (item in lambdaGen(3)) { lambdaResults << item }
+ assert lambdaResults == [100, 200, 300]
+ '''
+ }
+
+ @Test
+ void testYieldReturnExceptionConsistencyAllForms() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ // async method generator with error
+ class FailGen {
+ async failing() {
+ yield return 1
+ throw new java.io.IOException("gen method error")
+ }
+ }
+
+ // async closure generator with error
+ def closureFailGen = async {
+ yield return 10
+ throw new java.io.IOException("gen closure error")
+ }
+
+ // async lambda generator with error
+ def lambdaFailGen = async { x ->
+ yield return x
+ throw new java.io.IOException("gen lambda error")
+ }
+
+ // All should propagate IOException through for-await
+ for (gen in [new FailGen().failing(), closureFailGen, lambdaFailGen(100)]) {
+ def items = []
+ try {
+ for await (item in gen) { items << item }
+ assert false : "Should have thrown"
+ } catch (java.io.IOException e) {
+ assert e.message.contains("gen")
+ assert items.size() == 1
+ }
+ }
+ '''
+ }
+
+ // ---- executeAsync / executeAsyncVoid (unified path) ----
+
+ @Test
+ void testExecuteAsyncWithCustomExecutor() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.Executors
+
+ def pool = Executors.newSingleThreadExecutor({ r ->
+ def t = new Thread(r)
+ t.setName("exec-async-test")
+ t
+ })
+
+ def awaitable = executeAsync({ ->
+ Thread.currentThread().getName()
+ }, pool)
+ def result = await(awaitable)
+ assert result.startsWith("exec-async-test")
+ pool.shutdown()
+ '''
+ }
+
+ @Test
+ void testExecuteAsyncVoidWithCustomExecutor() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.Executors
+ import java.util.concurrent.atomic.AtomicBoolean
+
+ def pool = Executors.newSingleThreadExecutor()
+ def executed = new AtomicBoolean(false)
+
+ def awaitable = executeAsyncVoid({ ->
+ executed.set(true)
+ }, pool)
+ def _v1 = await(awaitable)
+ assert executed.get()
+ pool.shutdown()
+ '''
+ }
+
+ @Test
+ void testAsyncVoidMethodReturnsAwaitable() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.atomic.AtomicInteger
+
+ class VoidService {
+ static AtomicInteger counter = new AtomicInteger(0)
+
+ @Async
+ void increment() {
+ counter.incrementAndGet()
+ }
+ }
+
+ def svc = new VoidService()
+ def awaitable = svc.increment()
+ assert awaitable instanceof Awaitable
+ def _v2 = await(awaitable)
+ assert VoidService.counter.get() == 1
+ '''
+ }
+
+ // ---- Edge cases and coverage improvements ----
+
+ @Test
+ void testAwaitNull() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ def result = await((Object) null)
+ assert result == null
+ '''
+ }
+
+ @Test
+ void testAwaitAlreadyCompletedAwaitable() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def awaitable = Awaitable.of(42)
+ assert awaitable.isDone()
+ def result = await(awaitable)
+ assert result == 42
+ '''
+ }
+
+ @Test
+ void testAwaitFailedAwaitable() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def awaitable = Awaitable.failed(new RuntimeException("pre-failed"))
+ assert awaitable.isCompletedExceptionally()
+ try {
+ await(awaitable)
+ assert false
+ } catch (RuntimeException e) {
+ assert e.message == "pre-failed"
+ }
+ '''
+ }
+
+ @Test
+ void testAwaitCancelledAwaitable() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.GroovyPromise
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.CancellationException
+
+ def cf = new CompletableFuture()
+ def awaitable = GroovyPromise.of(cf)
+ awaitable.cancel()
+ assert awaitable.isCancelled()
+ try {
+ await(awaitable)
+ assert false
+ } catch (CancellationException e) {
+ // expected
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncWithReturnValue() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def awaitable = async { return "hello" }
+ assert await(awaitable) == "hello"
+ '''
+ }
+
+ @Test
+ void testAsyncWithNullReturnValue() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def awaitable = async { return null }
+ assert await(awaitable) == null
+ '''
+ }
+
+ @Test
+ void testMultipleConcurrentAsyncGenerators() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def gen1 = async { for (int i = 0; i < 5; i++) { yield return "A${i}" } }
+ def gen2 = async { for (int i = 0; i < 5; i++) { yield return "B${i}" } }
+
+ def results1 = []
+ def results2 = []
+ for await (item in gen1) { results1 << item }
+ for await (item in gen2) { results2 << item }
+ assert results1 == ["A0", "A1", "A2", "A3", "A4"]
+ assert results2 == ["B0", "B1", "B2", "B3", "B4"]
+ '''
+ }
+
+ @Test
+ void testEmptyAsyncStream() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class EmptyGen {
+ async empty() {
+ // A generator must have yield return to be detected as one.
+ // This has one but it's unreachable at runtime.
+ if (false) yield return "unreachable"
+ }
+ }
+
+ def results = []
+ for await (item in new EmptyGen().empty()) {
+ results << item
+ }
+ assert results.isEmpty()
+ '''
+ }
+
+ @Test
+ void testAwaitableCompositionWithThen() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def original = async { 10 }
+ def doubled = original.then { it * 2 }
+ def result = await(doubled)
+ assert result == 20
+ '''
+ }
+
+ @Test
+ void testAwaitableCompositionWithThenCompose() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def first = async { 5 }
+ def chained = first.thenCompose { v -> async { v + 10 } }
+ def result = await(chained)
+ assert result == 15
+ '''
+ }
+
+ @Test
+ void testAwaitableExceptionally() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def failing = async { throw new RuntimeException("oops") }
+ def recovered = failing.exceptionally { e -> "recovered: ${e.message}" }
+ def result = await(recovered)
+ assert result == "recovered: oops"
+ '''
+ }
+
+ @Test
+ void testAwaitWithCompletionStage() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletableFuture
+ import java.util.concurrent.CompletionStage
+
+ CompletionStage stage = CompletableFuture.supplyAsync { "from stage" }
+ def result = await(stage)
+ assert result == "from stage"
+ '''
+ }
+
+ @Test
+ void testAwaitWithFuture() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.Executors
+ import java.util.concurrent.Future
+
+ def pool = Executors.newSingleThreadExecutor()
+ Future future = pool.submit({ "from future" } as java.util.concurrent.Callable)
+ def result = await(future)
+ assert result == "from future"
+ pool.shutdown()
+ '''
+ }
+
+ @Test
+ void testAwaitAllWithMixedTypes() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ def a1 = async { 1 }
+ def a2 = CompletableFuture.supplyAsync { 2 }
+ def a3 = Awaitable.of(3)
+
+ def results = awaitAll(a1, a2, a3)
+ assert results == [1, 2, 3]
+ '''
+ }
+
+ @Test
+ void testAwaitAnyReturnsFirst() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ def fast = Awaitable.of("fast")
+ def slow = async { Thread.sleep(500); "slow" }
+
+ def result = awaitAny(fast, slow)
+ assert result == "fast"
+ '''
+ }
+
+ @Test
+ void testAwaitAllSettledMixed() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def success = async { "ok" }
+ def failure = async { throw new RuntimeException("fail") }
+
+ def results = awaitAllSettled(success, failure)
+ assert results.size() == 2
+ assert results[0].isSuccess()
+ assert results[0].getValue() == "ok"
+ assert results[1].isFailure()
+ assert results[1].getError().message == "fail"
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithPrimitiveReturnType() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class PrimService {
+ @Async
+ int computeInt() { return 42 }
+
+ @Async
+ boolean checkBool() { return true }
+
+ @Async
+ double computeDouble() { return 3.14 }
+ }
+
+ def svc = new PrimService()
+ assert await(svc.computeInt()) == 42
+ assert await(svc.checkBool()) == true
+ assert Math.abs(await(svc.computeDouble()) - 3.14) < 0.001
+ '''
+ }
+
+ @Test
+ void testDeepUnwrapNestedExceptions() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletionException
+ import java.util.concurrent.ExecutionException
+
+ // Create deeply nested exception chain
+ def original = new java.io.IOException("deep")
+ def wrapped = new CompletionException(new ExecutionException(
+ new java.lang.reflect.UndeclaredThrowableException(
+ new java.lang.reflect.InvocationTargetException(original))))
+
+ def cf = new java.util.concurrent.CompletableFuture()
+ cf.completeExceptionally(wrapped)
+
+ try {
+ await(cf)
+ assert false
+ } catch (java.io.IOException e) {
+ assert e.message == "deep"
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncWithNestedAwait() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class NestedService {
+ async inner(int v) { return v * 2 }
+
+ async outer(int v) {
+ def intermediate = await inner(v)
+ return await inner(intermediate)
+ }
+ }
+
+ def svc = new NestedService()
+ def result = await(svc.outer(5))
+ assert result == 20 // 5 * 2 * 2
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithNestedAwait() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ def inner = async { x -> x + 1 }
+ def outer = async {
+ def v1 = await(inner(10))
+ def v2 = await(inner(v1))
+ return v2
+ }
+ assert await(outer) == 12 // 10 + 1 + 1
+ '''
+ }
+
+ @Test
+ void testParallelAwaitInAsyncMethod() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.CompletableFuture
+
+ class ParallelService {
+ CompletableFuture<Integer> compute(int v) {
+ return CompletableFuture.supplyAsync {
+ Thread.sleep(50)
+ return v * 2
+ }
+ }
+
+ async parallel() {
+ def f1 = compute(1)
+ def f2 = compute(2)
+ def f3 = compute(3)
+ return await(f1) + await(f2) + await(f3)
+ }
+ }
+
+ def svc = new ParallelService()
+ long start = System.currentTimeMillis()
+ def result = await(svc.parallel())
+ long elapsed = System.currentTimeMillis() - start
+ assert result == 12 // 2 + 4 + 6
+ assert elapsed < 500 // parallel, not sequential
+ '''
+ }
+
+ @Test
+ void testToAsyncStreamConversion() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.AsyncStream
+ import groovy.concurrent.Awaitable
+ import groovy.concurrent.AwaitableAdapter
+ import groovy.concurrent.AwaitableAdapterRegistry
+
+ // Register an adapter for List that provides an AsyncStream
+ AwaitableAdapterRegistry.register(new AwaitableAdapter() {
+ boolean supportsAwaitable(Class type) { return false }
+ Awaitable toAwaitable(Object source) { return null }
+ boolean supportsAsyncStream(Class type) { return List.isAssignableFrom(type) }
+ AsyncStream toAsyncStream(Object source) {
+ def list = (List) source
+ def iter = list.iterator()
+ return new AsyncStream() {
+ def current
+ Awaitable<Boolean> moveNext() {
+ if (iter.hasNext()) {
+ current = iter.next()
+ return Awaitable.of(true)
+ }
+ return Awaitable.of(false)
+ }
+ Object getCurrent() { return current }
+ }
+ }
+ })
+
+ def results = []
+ def stream = toAsyncStream([10, 20, 30])
+ for await (item in stream) {
+ results << item
+ }
+ assert results == [10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testAwaitObjectDispatchesToCorrectOverload() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ // Test Object overload dispatch for each type
+ Object a = Awaitable.of("awaitable")
+ Object b = CompletableFuture.completedFuture("cf")
+ Object c = CompletableFuture.completedFuture("stage") as java.util.concurrent.CompletionStage
+
+ assert await(a) == "awaitable"
+ assert await(b) == "cf"
+ assert await(c) == "stage"
+ '''
+ }
+
+ @Test
+ void testAsyncMethodWithParameters() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+
+ class ParamService {
+ async add(int a, int b) {
+ return a + b
+ }
+
+ async greet(String name) {
+ return "Hello, ${name}!"
+ }
+ }
+
+ def svc = new ParamService()
+ assert await(svc.add(3, 4)) == 7
+ assert await(svc.greet("Groovy")) == "Hello, Groovy!"
+ '''
+ }
+
+ @Test
+ void testAsyncChainedMethods() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ class Pipeline {
+ async step1() { return 1 }
+
+ async step2(Awaitable<Integer> input) {
+ return await(input) + 10
+ }
+
+ async step3(Awaitable<Integer> input) {
+ return await(input) * 2
+ }
+ }
+
+ def p = new Pipeline()
+ def r1 = p.step1()
+ def r2 = p.step2(r1)
+ def r3 = p.step3(r2)
+ assert r3.get() == 22
+ '''
+ }
+
+ @Test
+ void testAwaitSyntaxWithBinaryOperations() {
+ assertScript '''
+ import groovy.transform.Async
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.Awaitable
+
+ class MathService {
+ async compute(int v) { return v }
+ }
+
+ def svc = new MathService()
+ // Test that await properly integrates with binary operators
+ def a = svc.compute(10)
+ def b = svc.compute(20)
+ def r1 = await(a)
+ def r2 = await(b)
+ assert r1 + r2 == 30
+ '''
+ }
+
+ @Test
+ void testAsyncClosureWithoutYieldReturnIsNotGenerator() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ // Closure with no yield return produces an Awaitable (not AsyncStream)
+ def result = async { 42 }
+ assert await(result) == 42
+
+ def resultNull = async { /* empty body */ }
+ assert await(resultNull) == null
+ '''
+ }
+
+ @Test
+ void testGroovyPromiseToString() {
+ assertScript '''
+ import groovy.concurrent.GroovyPromise
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ def pending = new CompletableFuture()
+ def promise = GroovyPromise.of(pending)
+ assert promise.toString() == "GroovyPromise{pending}"
+
+ pending.complete(42)
+ assert promise.toString() == "GroovyPromise{completed}"
+
+ def failed = GroovyPromise.of(CompletableFuture.failedFuture(new RuntimeException()))
+ assert failed.toString() == "GroovyPromise{failed}"
+ '''
+ }
+
+ @Test
+ void testGroovyPromiseGetWithTimeout() {
+ assertScript '''
+ import groovy.concurrent.GroovyPromise
+ import static groovy.transform.AsyncUtils.*
+ import java.util.concurrent.TimeUnit
+ import java.util.concurrent.TimeoutException
+
+ def awaitable = async { Thread.sleep(5000); "never" }
+ try {
+ awaitable.get(50, TimeUnit.MILLISECONDS)
+ assert false : "Should have timed out"
+ } catch (TimeoutException e) {
+ // expected
+ }
+ '''
+ }
+
+ // ---- for-await null safety ----
+
+ @Test
+ void testForAwaitNullSkipsLoop() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ class Gen {
+ async static test() {
+ def results = []
+ for await (item in null) {
+ results << item
+ }
+ return results
+ }
+ }
+ assert Gen.test().get() == []
+ '''
+ }
+
+ @Test
+ void testForAwaitNullWithExistingItems() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ class Gen {
+ async static test() {
+ def results = []
+ // Normal stream works
+ def stream = async { yield return 42 }
+ for await (item in stream) {
+ results << item
+ }
+ // Null stream skips
+ for await (item in null) {
+ results << "should not appear"
+ }
+ return results
+ }
+ }
+ assert Gen.test().get() == [42]
+ '''
+ }
+
+ @Test
+ void testForAwaitEmptyStream() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+ import groovy.concurrent.AsyncStream
+
+ class Gen {
+ async static test() {
+ def results = []
+ for await (item in AsyncStream.empty()) {
+ results << item
+ }
+ return results
+ }
+ }
+ assert Gen.test().get() == []
+ '''
+ }
+
+ // ---- ThreadLocal elimination verification ----
+
+ @Test
+ void testYieldReturnNoThreadLocal() {
+ assertScript '''
+ import static groovy.transform.AsyncUtils.*
+
+ // Verify that yield return works without ThreadLocal
+ // by running many concurrent generators simultaneously
+ class Gen {
+ async static generate(int id, int count) {
+ for (i in 1..count) {
+ yield return "${id}:${i}"
+ }
+ }
+ }
+
+ def streams = (1..10).collect { Gen.generate(it, 5) }
+ def allResults = []
+ for (stream in streams) {
+ def items = []
+ for await (item in stream) {
+ items << item
+ }
+ allResults << items
+ }
+
+ // Each generator produces exactly its items
+ for (i in 0..9) {
+ def expected = (1..5).collect { "${i+1}:${it}" }
+ assert allResults[i] == expected
+ }
+ '''
+ }
+
+ @Test
+ void testYieldReturnOutsideAsyncContextFails() {
+ assertScript '''
+ // yield return outside async context should fail at runtime
+ try {
+ groovy.transform.AsyncUtils.yieldReturn("oops")
+ assert false : "expected exception"
+ } catch (IllegalStateException e) {
+ assert e.message.contains("yield return")
+ }
+ '''
+ }
+}
diff --git a/versions.properties b/versions.properties
index 17c2aa09f6..af78e7ae37 100644
--- a/versions.properties
+++ b/versions.properties
@@ -51,6 +51,8 @@ logback=1.5.27
openbeans=1.0.2
picocli=4.7.7
qdox=2.2.0
+reactor=3.7.3
+rxjava3=3.1.10
slf4j=2.0.17
# running with Groovy 6 can be allowed with -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true
spock=2.4-groovy-5.0