This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch GROOVY-9381_3
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY-9381_3 by this push:
new 34f79c6591 Minor tweaks
34f79c6591 is described below
commit 34f79c65910e7425d85a6bfaf0d8e4a153eec4f8
Author: Daniel Sun <[email protected]>
AuthorDate: Sun Mar 29 16:52:08 2026 +0900
Minor tweaks
---
src/antlr/GroovyParser.g4 | 2 +-
.../apache/groovy/parser/antlr4/AstBuilder.java | 18 +-
.../groovy/transform/AsyncTransformHelper.java | 23 ++-
src/spec/doc/core-async-await.adoc | 40 +++-
src/spec/test/AsyncAwaitSpecTest.groovy | 58 +++++-
.../runtime/async/AsyncAwaitSyntaxTest.groovy | 202 +++++++++++++++++++++
6 files changed, 327 insertions(+), 16 deletions(-)
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index f65c5cea70..9f12e2b16f 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -784,7 +784,7 @@ expression
// async closure/lambda must come before postfixExpression to resolve the
ambiguities between async and method call, e.g. async { ... }
| ASYNC nls closureOrLambdaExpression
#asyncClosureExprAlt
- | AWAIT nls (LPAREN expression RPAREN | expression)
#awaitExprAlt
+ | AWAIT nls (LPAREN expression (COMMA nls expression)* RPAREN |
expression) #awaitExprAlt
// qualified names, array expressions, method invocation, post inc/dec
| postfixExpression
#postfixExprAlt
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 1aecc9b9e0..63480c073c 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -2998,10 +2998,22 @@ public class AstBuilder extends
GroovyParserBaseVisitor<Object> {
@Override
public Expression visitAwaitExprAlt(final AwaitExprAltContext ctx) {
- Expression expr = (Expression) this.visit(ctx.expression());
+ List<? extends ExpressionContext> exprCtxs = ctx.expression();
+ if (exprCtxs.size() == 1) {
+ Expression expr = (Expression) this.visit(exprCtxs.get(0));
+ return configureAST(
+ AsyncTransformHelper.buildAwaitCall(expr),
+ ctx);
+ }
+
+ // Multi-arg: await(p1, p2, ..., pn)
+ List<Expression> exprs = new ArrayList<>(exprCtxs.size());
+ for (ExpressionContext ec : exprCtxs) {
+ exprs.add((Expression) this.visit(ec));
+ }
return configureAST(
- AsyncTransformHelper.buildAwaitCall(expr),
- ctx);
+ AsyncTransformHelper.buildAwaitCall(new
ArgumentListExpression(exprs)),
+ ctx);
}
@Override
diff --git
a/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
index 2ed8f97613..728382ef28 100644
--- a/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
@@ -18,6 +18,7 @@
*/
package org.codehaus.groovy.transform;
+import groovy.concurrent.Awaitable;
import org.apache.groovy.runtime.async.AsyncSupport;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
@@ -38,6 +39,7 @@ import org.codehaus.groovy.syntax.Types;
import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
/**
@@ -86,6 +88,7 @@ public final class AsyncTransformHelper {
private static final String ASYNC_SUPPORT_CLASS =
AsyncSupport.class.getName();
private static final ClassNode ASYNC_SUPPORT_TYPE =
ClassHelper.makeWithoutCaching(AsyncSupport.class, false);
+ private static final ClassNode AWAITABLE_TYPE =
ClassHelper.makeWithoutCaching(Awaitable.class, false);
private static final String ASYNC_GEN_PARAM_NAME = "$__asyncGen__";
private static final String DEFER_SCOPE_VAR = "$__deferScope__";
@@ -115,14 +118,26 @@ public final class AsyncTransformHelper {
}
/**
- * Builds {@code AsyncSupport.await(arg)}.
- * Accepts either a single expression or a pre-assembled
- * {@link ArgumentListExpression}.
+ * Builds the AST for an {@code await} expression.
+ * <p>
+ * <b>Single-argument:</b> produces {@code AsyncSupport.await(arg)}.
+ * <br>
+ * <b>Multi-argument:</b> when {@code arg} is an {@link
ArgumentListExpression}
+ * with two or more entries, produces
+ * {@code AsyncSupport.await(Awaitable.all(arg1, arg2, ..., argN))},
+ * making {@code await(p1, p2, p3)} semantically equivalent to
+ * {@code await Awaitable.all(p1, p2, p3)}.
*
- * @param arg the expression to await
+ * @param arg the expression(s) to await — a single expression or a
+ * pre-assembled {@link ArgumentListExpression}
* @return an AST node representing the static call
*/
public static Expression buildAwaitCall(Expression arg) {
+ if (arg instanceof ArgumentListExpression args &&
args.getExpressions().size() > 1) {
+ // Multi-arg: await(p1, p2, ..., pn) → await(Awaitable.all(p1, p2,
..., pn))
+ Expression allCall = callX(classX(AWAITABLE_TYPE), "all", args);
+ return callX(ASYNC_SUPPORT_TYPE, AWAIT_METHOD, new
ArgumentListExpression(allCall));
+ }
return callX(ASYNC_SUPPORT_TYPE, AWAIT_METHOD, ensureArgs(arg));
}
diff --git a/src/spec/doc/core-async-await.adoc
b/src/spec/doc/core-async-await.adoc
index 190dd9be06..105bb7a77b 100644
--- a/src/spec/doc/core-async-await.adoc
+++ b/src/spec/doc/core-async-await.adoc
@@ -380,6 +380,39 @@ The parenthesized form `await(expr)` is recommended when
`await` is used in comp
(e.g., `await(f1) + await(f2)`) to avoid potential ambiguities.
====
+=== Multi-Argument `await` — Parallel Awaiting
+
+The parenthesized form supports multiple comma-separated arguments:
+
+[source,groovy]
+----
+def (user, orders) = await(userTask, orderTask)
+----
+
+This is semantically equivalent to `await Awaitable.all(...)` — all arguments
are
+awaited concurrently and results are returned as a `List` in the same order as
the
+arguments. This is Groovy's counterpart to JavaScript's `await
Promise.all(...)`.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=multi_arg_await_paren,indent=0]
+----
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=multi_arg_await_operator,indent=0]
+----
+
+[IMPORTANT]
+====
+Multi-argument `await` requires explicit parentheses: `await(a, b, c)`.
+The unparenthesized form `await a` accepts only a single expression — commas
after
+it are interpreted as part of the enclosing syntax (list literals, argument
lists,
+variable declarations). This design preserves the natural meaning of
+`[await a, await b]` (a list of two separately awaited values) and matches the
+convention of all other mainstream languages.
+====
+
[[async-closures-lambdas]]
== Async Closures and Lambdas
@@ -1320,7 +1353,7 @@ JavaScript, C#, Kotlin, and Swift, for developers
familiar with those languages.
| `func foo() async throws -> T`
| **Await expression**
-| `await expr` / `await(expr)`
+| `await expr` / `await(expr)` / `await(a, b, c)`
| `await expr`
| `await expr`
| `deferred.await()` / suspend call
@@ -2060,6 +2093,9 @@ is likewise an interface, with the default implementation
provided internally.
| Await expression
| `await expr` or `await(expr)`
+| Multi-argument await
+| `await(a, b, c)` — equivalent to `await Awaitable.all(a, b, c)`
+
| For await
| `for await (item in source) { ... }` or `for await (item : source) { ... }`
@@ -2070,7 +2106,7 @@ is likewise an interface, with the default implementation
provided internally.
| `defer { cleanup code }`
| Parallel wait
-| `await Awaitable.all(a, b, c)`
+| `await(a, b, c)` or `await Awaitable.all(a, b, c)`
| Race
| `await Awaitable.any(a, b)`
diff --git a/src/spec/test/AsyncAwaitSpecTest.groovy
b/src/spec/test/AsyncAwaitSpecTest.groovy
index 58d6478358..bc5959b22e 100644
--- a/src/spec/test/AsyncAwaitSpecTest.groovy
+++ b/src/spec/test/AsyncAwaitSpecTest.groovy
@@ -680,7 +680,7 @@ def observed = new AtomicReference()
await Awaitable.of("groovy").thenAccept { observed.set(it.toUpperCase()) }
assert observed.get() == "GROOVY"
-def recovered = await
+def recovered = await
Awaitable.failed(new IOException("boom"))
.handle { value, error -> "recovered from ${error.message}" }
@@ -842,7 +842,7 @@ assert name.startsWith('pool-')
import groovy.concurrent.Awaitable
import groovy.concurrent.AsyncChannel
-async def racingExample() {
+async racingExample() {
def ch = AsyncChannel.create(1)
Awaitable.go {
@@ -1524,7 +1524,7 @@ assert result == "USER-42: PREMIUM"
import groovy.concurrent.Awaitable
// ------ After: sequential async/await (same logic, flat and readable) ------
-async def fetchProfile(int userId) {
+async fetchProfile(int userId) {
def name = await Awaitable.of("User-${userId}")
def profile = await Awaitable.of("${name}: premium")
return profile.toUpperCase()
@@ -1541,7 +1541,7 @@ assert await(fetchProfile(42)) == "USER-42: PREMIUM"
// tag::motivation_exception_handling[]
import groovy.concurrent.Awaitable
-async def riskyOperation() {
+async riskyOperation() {
throw new IOException("network timeout")
}
@@ -1563,7 +1563,7 @@ try {
import groovy.concurrent.Awaitable
// Async generator produces values on-demand with back-pressure
-async def fibonacci(int count) {
+async fibonacci(int count) {
long a = 0, b = 1
for (int i = 0; i < count; i++) {
yield return a
@@ -1589,7 +1589,7 @@ import groovy.concurrent.Awaitable
// Groovy's await understands CompletableFuture, CompletionStage,
// Future, and Flow.Publisher out of the box — no conversion needed
-async def mixedSources() {
+async mixedSources() {
def fromCF = await CompletableFuture.supplyAsync { "cf" }
def fromAwaitable = await Awaitable.of("awaitable")
return "${fromCF}+${fromAwaitable}"
@@ -1974,4 +1974,50 @@ assert publisherItems == [100, 200, 300]
// end::channel_vs_flow_for_await[]
'''
}
+
+ @Test
+ void testMultiArgAwaitParenthesized() {
+ assertScript '''
+// tag::multi_arg_await_paren[]
+import groovy.concurrent.Awaitable
+
+async fetchUserAndOrders(long userId) {
+ def userTask = Awaitable.go { fetchUser(userId) }
+ def orderTask = Awaitable.go { fetchOrders(userId) }
+
+ // await multiple awaitables — equivalent to await Awaitable.all(...)
+ def (user, orders) = await(userTask, orderTask)
+ return [user: user, orders: orders]
+}
+// end::multi_arg_await_paren[]
+
+def fetchUser(id) { "user-$id" }
+def fetchOrders(id) { ["order-1", "order-2"] }
+
+def result = await fetchUserAndOrders(42)
+assert result.user == 'user-42'
+assert result.orders == ['order-1', 'order-2']
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitOperator() {
+ assertScript '''
+// tag::multi_arg_await_operator[]
+import groovy.concurrent.Awaitable
+
+async parallelCompute() {
+ def a = Awaitable.go { 10 * 10 }
+ def b = Awaitable.go { 20 * 20 }
+ def c = Awaitable.go { 30 * 30 }
+
+ // Multi-arg await with explicit parentheses
+ def results = await(a, b, c)
+ return results
+}
+// end::multi_arg_await_operator[]
+
+assert await(parallelCompute()) == [100, 400, 900]
+ '''
+ }
}
diff --git
a/src/test/groovy/org/apache/groovy/runtime/async/AsyncAwaitSyntaxTest.groovy
b/src/test/groovy/org/apache/groovy/runtime/async/AsyncAwaitSyntaxTest.groovy
index 6ccdf2fa51..7a2e8ce95d 100644
---
a/src/test/groovy/org/apache/groovy/runtime/async/AsyncAwaitSyntaxTest.groovy
+++
b/src/test/groovy/org/apache/groovy/runtime/async/AsyncAwaitSyntaxTest.groovy
@@ -2920,4 +2920,206 @@ class AsyncAwaitSyntaxTest {
'''
}
+ // ---- Multi-argument await
-----------------------------------------------
+
+ @Test
+ void testMultiArgAwaitParenthesizedForm() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async fetchAll() {
+ def p1 = Awaitable.of('alpha')
+ def p2 = Awaitable.of('beta')
+ def p3 = Awaitable.of('gamma')
+ def results = await(p1, p2, p3)
+ return results
+ }
+
+ def results = await fetchAll()
+ assert results == ['alpha', 'beta', 'gamma']
+ assert results.size() == 3
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitEquivalentToAll() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async fetchViaParen() {
+ def p1 = Awaitable.of(10)
+ def p2 = Awaitable.of(20)
+ def p3 = Awaitable.of(30)
+ return await(p1, p2, p3)
+ }
+
+ async fetchViaAll() {
+ def p1 = Awaitable.of(10)
+ def p2 = Awaitable.of(20)
+ def p3 = Awaitable.of(30)
+ return await Awaitable.all(p1, p2, p3)
+ }
+
+ // Both forms produce the same result
+ assert await(fetchViaParen()) == [10, 20, 30]
+ assert await(fetchViaAll()) == [10, 20, 30]
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitTwoArgs() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async fetchPair() {
+ def (a, b) = await(Awaitable.of('hello'),
Awaitable.of('world'))
+ return "$a $b"
+ }
+
+ assert await(fetchPair()) == 'hello world'
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitPreservesOrder() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async ordered() {
+ // slower task first, faster task second — results must match
argument order
+ def slow = Awaitable.go { Thread.sleep(50); 'slow' }
+ def fast = Awaitable.of('fast')
+ return await(slow, fast)
+ }
+
+ def results = await ordered()
+ assert results[0] == 'slow'
+ assert results[1] == 'fast'
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitPropagatesFirstFailure() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async willFail() {
+ def ok = Awaitable.of(1)
+ def bad = Awaitable.failed(new IOException('network error'))
+ return await(ok, bad)
+ }
+
+ try {
+ await willFail()
+ assert false : 'should not reach here'
+ } catch (IOException e) {
+ assert e.message == 'network error'
+ }
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitWithMixedTypes() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import java.util.concurrent.CompletableFuture
+
+ async mixed() {
+ def awaitable = Awaitable.of('from-awaitable')
+ def cf = CompletableFuture.completedFuture('from-cf')
+ return await(awaitable, cf)
+ }
+
+ def results = await mixed()
+ assert results == ['from-awaitable', 'from-cf']
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitInAsyncClosure() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ def task = async {
+ def a = Awaitable.of(1)
+ def b = Awaitable.of(2)
+ def c = Awaitable.of(3)
+ def r = await(a, b, c)
+ r.sum()
+ }
+ assert await(task()) == 6
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitInReturnStatement() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async compute() {
+ return await(Awaitable.of(100), Awaitable.of(200))
+ }
+
+ assert await(compute()) == [100, 200]
+ '''
+ }
+
+ @Test
+ void testSingleArgAwaitStillWorks() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async single() {
+ return await Awaitable.of(42)
+ }
+ assert await(single()) == 42
+ '''
+ }
+
+ @Test
+ void testSingleArgAwaitParenStillWorks() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async single() {
+ return await(Awaitable.of('hello'))
+ }
+ assert await(single()) == 'hello'
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitWithDestructuring() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+
+ async destruct() {
+ def (x, y, z) = await(Awaitable.of(1), Awaitable.of(2),
Awaitable.of(3))
+ return x + y + z
+ }
+ assert await(destruct()) == 6
+ '''
+ }
+
+ @Test
+ void testMultiArgAwaitViaAsyncMethodCompileStatic() {
+ assertScript '''
+ import groovy.concurrent.Awaitable
+ import groovy.transform.Async
+ import groovy.transform.CompileStatic
+
+ @CompileStatic
+ class Service {
+ async List<String> fetchAll() {
+ def p1 = Awaitable.of('a')
+ def p2 = Awaitable.of('b')
+ def p3 = Awaitable.of('c')
+ return await(p1, p2, p3)
+ }
+ }
+
+ def svc = new Service()
+ assert await(svc.fetchAll()) == ['a', 'b', 'c']
+ '''
+ }
}