Skip to content

Commit d71eab9

Browse files
committed
GROOVY-9381: Support async/await like ES7
1 parent a0feded commit d71eab9

25 files changed

+9879
-3
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ dependencies {
122122
testImplementation "com.thoughtworks.qdox:qdox:${versions.qdox}"
123123
testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
124124
testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
125+
testImplementation "io.reactivex.rxjava3:rxjava:${versions.rxjava3}"
126+
testImplementation "io.projectreactor:reactor-core:${versions.reactor}"
125127

126128
testFixturesImplementation projects.groovyXml
127129
testFixturesImplementation projects.groovyTest

gradle/verification-metadata.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
<ignored-key id="C71FB765CD9DE313" reason="Key couldn't be downloaded from any key server"/>
115115
<ignored-key id="C7CA19B7B620D787" reason="Key couldn't be downloaded from any key server"/>
116116
<ignored-key id="CA80D1F0EB6CA4BA" reason="Key couldn't be downloaded from any key server"/>
117+
<ignored-key id="D1031D14464180E0" reason="Key couldn't be downloaded from any key server"/>
117118
<ignored-key id="D2151178A123C97F" reason="Key couldn't be downloaded from any key server"/>
118119
<ignored-key id="D364ABAA39A47320" reason="Key couldn't be downloaded from any key server"/>
119120
<ignored-key id="D7742D58455ECC7C" reason="Key couldn't be downloaded from any key server"/>
@@ -851,6 +852,16 @@
851852
<sha512 value="f220e44fe6b61f8dbb61226f832dfb16a09584384540fd48a4dff5c4de9fee060623f85cbead720dfe776aa25105949e70758a9bb1d9db43f63068d8d22164c9" origin="Generated by Gradle"/>
852853
</artifact>
853854
</component>
855+
<component group="io.projectreactor" name="reactor-core" version="3.7.3">
856+
<artifact name="reactor-core-3.7.3.jar">
857+
<pgp value="48B086A7D843CFA258E83286928FBF39003C0425"/>
858+
</artifact>
859+
</component>
860+
<component group="io.reactivex.rxjava3" name="rxjava" version="3.1.10">
861+
<artifact name="rxjava-3.1.10.jar">
862+
<pgp value="E9CC3CD1AE59E851E4DB3FA350FFD7487D34B5B9"/>
863+
</artifact>
864+
</component>
854865
<component group="jakarta.activation" name="jakarta.activation-api" version="1.2.1">
855866
<artifact name="jakarta.activation-api-1.2.1.jar">
856867
<pgp value="6DD3B8C64EF75253BEB2C53AD908A43FB7EC07AC"/>
@@ -2206,6 +2217,11 @@
22062217
<sha512 value="adcc480f68828ffd68d03846be852988b595c2e1bb69224d273578dd6c2ad2773edfe96625a7c00bc40ae0f2d1cac8412eaa54b88cc8e681b0b4c0ee3b082333" origin="Generated by Gradle"/>
22072218
</artifact>
22082219
</component>
2220+
<component group="org.reactivestreams" name="reactive-streams" version="1.0.4">
2221+
<artifact name="reactive-streams-1.0.4.jar">
2222+
<sha512 value="cdab6bd156f39106cd6bbfd47df1f4b0a89dc4aa28c68c31ef12a463193c688897e415f01b8d7f0d487b0e6b5bd2f19044bf8605704b024f26d6aa1f4f9a2471" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
2223+
</artifact>
2224+
</component>
22092225
<component group="org.reflections" name="reflections" version="0.10.2">
22102226
<artifact name="reflections-0.10.2.jar">
22112227
<pgp value="3F2A008A91D11A7FAC4A0786F13D3E721D56BD54"/>

src/antlr/GroovyLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ DEF : 'def';
392392
IN : 'in';
393393
TRAIT : 'trait';
394394
THREADSAFE : 'threadsafe'; // reserved keyword
395+
ASYNC : 'async';
396+
AWAIT : 'await';
395397

396398
// §3.9 Keywords
397399
BuiltInPrimitiveType

src/antlr/GroovyParser.g4

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ modifier
134134
| VOLATILE
135135
| DEF
136136
| VAR
137+
| ASYNC
137138
)
138139
;
139140

@@ -600,7 +601,7 @@ switchStatement
600601
;
601602

602603
loopStatement
603-
: FOR LPAREN forControl RPAREN nls statement #forStmtAlt
604+
: FOR AWAIT? LPAREN forControl RPAREN nls statement #forStmtAlt
604605
| WHILE expressionInPar nls statement #whileStmtAlt
605606
| DO nls statement nls WHILE expressionInPar #doWhileStmtAlt
606607
;
@@ -642,6 +643,7 @@ statement
642643
| continueStatement #continueStmtAlt
643644
| { inSwitchExpressionLevel > 0 }?
644645
yieldStatement #yieldStmtAlt
646+
| YIELD RETURN nls expression #yieldReturnStmtAlt
645647
| identifier COLON nls statement #labeledStmtAlt
646648
| assertStatement #assertStmtAlt
647649
| localVariableDeclaration #localVariableDeclarationStmtAlt
@@ -778,13 +780,17 @@ expression
778780
// must come before postfixExpression to resolve the ambiguities between casting and call on parentheses expression, e.g. (int)(1 / 2)
779781
: castParExpression castOperandExpression #castExprAlt
780782

783+
// async closure/lambda must come before postfixExpression to resolve the ambiguities between async and method call, e.g. async { ... }
784+
| ASYNC nls closureOrLambdaExpression #asyncClosureExprAlt
785+
781786
// qualified names, array expressions, method invocation, post inc/dec
782787
| postfixExpression #postfixExprAlt
783788

784789
| switchExpression #switchExprAlt
785790

786-
// ~(BNOT)/!(LNOT) (level 1)
791+
// ~(BNOT)/!(LNOT)/await (level 1)
787792
| (BITNOT | NOT) nls expression #unaryNotExprAlt
793+
| AWAIT nls expression #awaitExprAlt
788794

789795
// math power operator (**) (level 2)
790796
| left=expression op=POWER nls right=expression #powerExprAlt
@@ -1228,6 +1234,8 @@ identifier
12281234
: Identifier
12291235
| CapitalizedIdentifier
12301236
| AS
1237+
| ASYNC
1238+
| AWAIT
12311239
| IN
12321240
| PERMITS
12331241
| RECORD
@@ -1246,6 +1254,8 @@ keywords
12461254
: ABSTRACT
12471255
| AS
12481256
| ASSERT
1257+
| ASYNC
1258+
| AWAIT
12491259
| BREAK
12501260
| CASE
12511261
| CATCH
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package groovy.concurrent;
20+
21+
/**
22+
* Asynchronous iteration abstraction, analogous to C#'s
23+
* {@code IAsyncEnumerable<T>} or JavaScript's async iterables.
24+
* <p>
25+
* Used with the {@code for await} syntax:
26+
* <pre>
27+
* for await (item in asyncStream) {
28+
* process(item)
29+
* }
30+
* </pre>
31+
* <p>
32+
* Third-party reactive types (Reactor {@code Flux}, RxJava {@code Observable})
33+
* can be adapted to {@code AsyncStream} via {@link AwaitableAdapter}.
34+
*
35+
* @param <T> the element type
36+
* @see AwaitableAdapter
37+
* @since 6.0.0
38+
*/
39+
public interface AsyncStream<T> {
40+
41+
/**
42+
* Asynchronously advances to the next element. Returns an {@link Awaitable}
43+
* that completes with {@code true} if an element is available, or
44+
* {@code false} if the stream is exhausted.
45+
*/
46+
Awaitable<Boolean> moveNext();
47+
48+
/**
49+
* Returns the current element. Must only be called after {@link #moveNext()}
50+
* has completed with {@code true}.
51+
*/
52+
T getCurrent();
53+
54+
/**
55+
* Returns an empty {@code AsyncStream} that completes immediately.
56+
*/
57+
@SuppressWarnings("unchecked")
58+
static <T> AsyncStream<T> empty() {
59+
return (AsyncStream<T>) EMPTY;
60+
}
61+
62+
/** Singleton empty stream instance. */
63+
AsyncStream<Object> EMPTY = new AsyncStream<>() {
64+
@Override public Awaitable<Boolean> moveNext() { return Awaitable.of(false); }
65+
@Override public Object getCurrent() { return null; }
66+
};
67+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package groovy.concurrent;
20+
21+
import groovy.lang.Closure;
22+
import org.apache.groovy.runtime.async.AsyncSupport;
23+
24+
import java.util.List;
25+
import java.util.concurrent.CompletableFuture;
26+
import java.util.concurrent.CompletionStage;
27+
import java.util.concurrent.Executor;
28+
import java.util.concurrent.Future;
29+
30+
/**
31+
* User-facing API for Groovy's {@code async}/{@code await} language feature.
32+
* <p>
33+
* Provides methods for awaiting asynchronous results, creating async
34+
* computations, and configuring the async executor. Inside {@code async}
35+
* methods the {@code await expr} expression is compiled to call the internal
36+
* runtime support directly, but users may also call these methods explicitly:
37+
* <pre>
38+
* import static groovy.concurrent.AsyncUtils.*
39+
*
40+
* def future = async {
41+
* def result = await someAsyncCall()
42+
* return result * 2
43+
* }
44+
* </pre>
45+
* <p>
46+
* <b>Thread pool configuration:</b> On JDK 21+ the default executor uses
47+
* virtual threads. On earlier JDKs a dedicated daemon thread pool is used,
48+
* whose size is controlled by the system property
49+
* {@code groovy.async.parallelism} (default: {@code availableProcessors + 1}).
50+
* The executor can be overridden at any time via {@link #setExecutor}.
51+
*
52+
* @see groovy.transform.Async
53+
* @see Awaitable
54+
* @see AwaitableAdapterRegistry
55+
* @since 6.0.0
56+
*/
57+
public class AsyncUtils {
58+
59+
private AsyncUtils() { }
60+
61+
// ---- await overloads ------------------------------------------------
62+
63+
/**
64+
* Awaits the result of an {@link Awaitable}.
65+
* <p>
66+
* Exception handling follows the same principle as C# and JavaScript:
67+
* the <em>original</em> exception is rethrown transparently, even if it
68+
* is a checked exception.
69+
*/
70+
public static <T> T await(Awaitable<T> awaitable) {
71+
return AsyncSupport.await(awaitable);
72+
}
73+
74+
/**
75+
* Awaits the result of a {@link CompletableFuture}.
76+
*/
77+
public static <T> T await(CompletableFuture<T> future) {
78+
return AsyncSupport.await(future);
79+
}
80+
81+
/**
82+
* Awaits the result of a {@link CompletionStage}.
83+
*/
84+
public static <T> T await(CompletionStage<T> stage) {
85+
return AsyncSupport.await(stage);
86+
}
87+
88+
/**
89+
* Awaits the result of a {@link Future}.
90+
*/
91+
public static <T> T await(Future<T> future) {
92+
return AsyncSupport.await(future);
93+
}
94+
95+
/**
96+
* Awaits an arbitrary object by adapting it to {@link Awaitable} via the
97+
* {@link AwaitableAdapterRegistry}.
98+
*/
99+
public static <T> T await(Object source) {
100+
return AsyncSupport.await(source);
101+
}
102+
103+
// ---- async ----------------------------------------------------------
104+
105+
/**
106+
* Executes the given closure asynchronously using the default executor,
107+
* returning an {@link Awaitable}.
108+
*/
109+
public static <T> Awaitable<T> async(Closure<T> closure) {
110+
return AsyncSupport.async(closure);
111+
}
112+
113+
// ---- awaitAll / awaitAny / awaitAllSettled ---------------------------
114+
115+
/**
116+
* Waits for all given awaitables and returns their results as a list.
117+
* Analogous to JavaScript's {@code Promise.all()}.
118+
*/
119+
public static List<Object> awaitAll(Object... awaitables) {
120+
return AsyncSupport.awaitAll(awaitables);
121+
}
122+
123+
/**
124+
* Waits for any one of the given awaitables and returns its result.
125+
* Analogous to JavaScript's {@code Promise.race()}.
126+
*/
127+
public static Object awaitAny(Object... awaitables) {
128+
return AsyncSupport.awaitAny(awaitables);
129+
}
130+
131+
/**
132+
* Waits for all given awaitables and returns a list of {@link AwaitResult}
133+
* objects. Analogous to JavaScript's {@code Promise.allSettled()}.
134+
*/
135+
public static List<AwaitResult<Object>> awaitAllSettled(Object... awaitables) {
136+
return AsyncSupport.awaitAllSettled(awaitables);
137+
}
138+
139+
// ---- for-await support ----------------------------------------------
140+
141+
/**
142+
* Converts an arbitrary source to an {@link AsyncStream} via the adapter
143+
* registry. Useful for manual iteration with {@code for await} loops.
144+
* Returns an empty stream for {@code null} input.
145+
*/
146+
public static <T> AsyncStream<T> toAsyncStream(Object source) {
147+
return AsyncSupport.toAsyncStream(source);
148+
}
149+
150+
// ---- async execution ------------------------------------------------
151+
152+
/**
153+
* Executes the given closure asynchronously on the specified executor,
154+
* returning an {@link Awaitable}.
155+
*/
156+
public static <T> Awaitable<T> executeAsync(Closure<T> closure, Executor executor) {
157+
return AsyncSupport.executeAsync(closure, executor);
158+
}
159+
160+
/**
161+
* Void variant of {@link #executeAsync(Closure, Executor)} for void methods.
162+
*/
163+
public static Awaitable<Void> executeAsyncVoid(Closure<?> closure, Executor executor) {
164+
return AsyncSupport.executeAsyncVoid(closure, executor);
165+
}
166+
167+
// ---- exception utilities --------------------------------------------
168+
169+
/**
170+
* Deeply unwraps nested exception wrapper layers to find the original exception.
171+
*/
172+
public static Throwable deepUnwrap(Throwable t) {
173+
return AsyncSupport.deepUnwrap(t);
174+
}
175+
176+
// ---- executor configuration -----------------------------------------
177+
178+
/**
179+
* Returns {@code true} if the running JVM supports virtual threads (JDK 21+).
180+
*/
181+
public static boolean isVirtualThreadsAvailable() {
182+
return AsyncSupport.isVirtualThreadsAvailable();
183+
}
184+
185+
/**
186+
* Returns the current executor used for async operations.
187+
*/
188+
public static Executor getExecutor() {
189+
return AsyncSupport.getExecutor();
190+
}
191+
192+
/**
193+
* Sets the executor to use for async operations. Pass {@code null}
194+
* to reset to the default (virtual thread executor on JDK 21+,
195+
* or a dedicated daemon thread pool whose size is controlled by the
196+
* system property {@code groovy.async.parallelism}).
197+
*/
198+
public static void setExecutor(Executor executor) {
199+
AsyncSupport.setExecutor(executor);
200+
}
201+
}

0 commit comments

Comments
 (0)