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']
+        '''
+    }
 }

Reply via email to