Skip to content

Commit 5df6414

Browse files
committed
GROOVY-9381: Add native async/await support
1 parent a2a7da6 commit 5df6414

32 files changed

+20525
-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"/>
@@ -861,6 +862,16 @@
861862
<sha512 value="f220e44fe6b61f8dbb61226f832dfb16a09584384540fd48a4dff5c4de9fee060623f85cbead720dfe776aa25105949e70758a9bb1d9db43f63068d8d22164c9" origin="Generated by Gradle"/>
862863
</artifact>
863864
</component>
865+
<component group="io.projectreactor" name="reactor-core" version="3.7.3">
866+
<artifact name="reactor-core-3.7.3.jar">
867+
<pgp value="48B086A7D843CFA258E83286928FBF39003C0425"/>
868+
</artifact>
869+
</component>
870+
<component group="io.reactivex.rxjava3" name="rxjava" version="3.1.10">
871+
<artifact name="rxjava-3.1.10.jar">
872+
<pgp value="E9CC3CD1AE59E851E4DB3FA350FFD7487D34B5B9"/>
873+
</artifact>
874+
</component>
864875
<component group="jakarta.activation" name="jakarta.activation-api" version="1.2.1">
865876
<artifact name="jakarta.activation-api-1.2.1.jar">
866877
<pgp value="6DD3B8C64EF75253BEB2C53AD908A43FB7EC07AC"/>
@@ -2216,6 +2227,11 @@
22162227
<sha512 value="adcc480f68828ffd68d03846be852988b595c2e1bb69224d273578dd6c2ad2773edfe96625a7c00bc40ae0f2d1cac8412eaa54b88cc8e681b0b4c0ee3b082333" origin="Generated by Gradle"/>
22172228
</artifact>
22182229
</component>
2230+
<component group="org.reactivestreams" name="reactive-streams" version="1.0.4">
2231+
<artifact name="reactive-streams-1.0.4.jar">
2232+
<sha512 value="cdab6bd156f39106cd6bbfd47df1f4b0a89dc4aa28c68c31ef12a463193c688897e415f01b8d7f0d487b0e6b5bd2f19044bf8605704b024f26d6aa1f4f9a2471" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
2233+
</artifact>
2234+
</component>
22192235
<component group="org.reflections" name="reflections" version="0.10.2">
22202236
<artifact name="reflections-0.10.2.jar">
22212237
<pgp value="3F2A008A91D11A7FAC4A0786F13D3E721D56BD54"/>

src/antlr/GroovyLexer.g4

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

396399
// §3.9 Keywords
397400
BuiltInPrimitiveType

src/antlr/GroovyParser.g4

Lines changed: 14 additions & 1 deletion
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,8 @@ statement
642643
| continueStatement #continueStmtAlt
643644
| { inSwitchExpressionLevel > 0 }?
644645
yieldStatement #yieldStmtAlt
646+
| YIELD RETURN nls expression #yieldReturnStmtAlt
647+
| DEFER nls statementExpression #deferStmtAlt
645648
| identifier COLON nls statement #labeledStmtAlt
646649
| assertStatement #assertStmtAlt
647650
| localVariableDeclaration #localVariableDeclarationStmtAlt
@@ -778,6 +781,10 @@ expression
778781
// must come before postfixExpression to resolve the ambiguities between casting and call on parentheses expression, e.g. (int)(1 / 2)
779782
: castParExpression castOperandExpression #castExprAlt
780783

784+
// async closure/lambda must come before postfixExpression to resolve the ambiguities between async and method call, e.g. async { ... }
785+
| ASYNC nls closureOrLambdaExpression #asyncClosureExprAlt
786+
| AWAIT nls (LPAREN expression RPAREN | expression) #awaitExprAlt
787+
781788
// qualified names, array expressions, method invocation, post inc/dec
782789
| postfixExpression #postfixExprAlt
783790

@@ -1228,6 +1235,9 @@ identifier
12281235
: Identifier
12291236
| CapitalizedIdentifier
12301237
| AS
1238+
| ASYNC
1239+
| AWAIT
1240+
| DEFER
12311241
| IN
12321242
| PERMITS
12331243
| RECORD
@@ -1246,12 +1256,15 @@ keywords
12461256
: ABSTRACT
12471257
| AS
12481258
| ASSERT
1259+
| ASYNC
1260+
| AWAIT
12491261
| BREAK
12501262
| CASE
12511263
| CATCH
12521264
| CLASS
12531265
| CONST
12541266
| CONTINUE
1267+
| DEFER
12551268
| DEF
12561269
| DEFAULT
12571270
| DO
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
* An {@code AsyncStream} can be produced in several ways:
33+
* <ul>
34+
* <li>Using {@code yield return} inside an {@code async} method or closure
35+
* to create a generator-style stream</li>
36+
* <li>Adapting JDK {@link java.util.concurrent.Flow.Publisher} instances
37+
* (supported out of the box by the built-in adapter)</li>
38+
* <li>Adapting third-party reactive types (Reactor {@code Flux}, RxJava
39+
* {@code Observable}) via {@link AwaitableAdapter}</li>
40+
* </ul>
41+
*
42+
* @param <T> the element type
43+
* @see AwaitableAdapter
44+
* @see AwaitableAdapterRegistry
45+
* @since 6.0.0
46+
*/
47+
public interface AsyncStream<T> extends AutoCloseable {
48+
49+
/**
50+
* Asynchronously advances to the next element. Returns an {@link Awaitable}
51+
* that completes with {@code true} if an element is available, or
52+
* {@code false} if the stream is exhausted.
53+
*/
54+
Awaitable<Boolean> moveNext();
55+
56+
/**
57+
* Returns the current element. Must only be called after {@link #moveNext()}
58+
* has completed with {@code true}.
59+
*/
60+
T getCurrent();
61+
62+
/**
63+
* Closes the stream and releases any associated resources.
64+
* <p>
65+
* The default implementation is a no-op. Implementations that bridge to
66+
* generators, publishers, or other resource-owning sources may override
67+
* this to propagate cancellation upstream. Compiler-generated
68+
* {@code for await} loops invoke {@code close()} automatically from a
69+
* {@code finally} block, including on early {@code break}, {@code return},
70+
* and exceptional exit.
71+
*
72+
* @since 6.0.0
73+
*/
74+
@Override
75+
default void close() {
76+
}
77+
78+
/**
79+
* Returns an empty {@code AsyncStream} that completes immediately.
80+
*/
81+
@SuppressWarnings("unchecked")
82+
static <T> AsyncStream<T> empty() {
83+
return (AsyncStream<T>) EMPTY;
84+
}
85+
86+
/**
87+
* Singleton empty stream instance.
88+
* <p>
89+
* This is an implementation detail backing {@link #empty()}.
90+
* User code should call {@code AsyncStream.empty()} rather than
91+
* referencing this field directly.
92+
*/
93+
AsyncStream<Object> EMPTY = new AsyncStream<>() {
94+
@Override public Awaitable<Boolean> moveNext() { return Awaitable.of(false); }
95+
@Override public Object getCurrent() { return null; }
96+
};
97+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 java.util.Objects;
22+
import java.util.function.Function;
23+
24+
/**
25+
* Represents the outcome of an asynchronous computation that may have
26+
* succeeded or failed. This is used by {@code awaitAllSettled()} —
27+
* the Groovy equivalent of JavaScript's {@code Promise.allSettled()}.
28+
* <p>
29+
* An {@code AwaitResult} is either a {@linkplain #isSuccess() success}
30+
* carrying a value, or a {@linkplain #isFailure() failure} carrying a
31+
* {@link Throwable}.
32+
*
33+
* @param <T> the value type
34+
* @since 6.0.0
35+
*/
36+
public final class AwaitResult<T> {
37+
38+
private final T value;
39+
private final Throwable error;
40+
private final boolean success;
41+
42+
private AwaitResult(T value, Throwable error, boolean success) {
43+
this.value = value;
44+
this.error = error;
45+
this.success = success;
46+
}
47+
48+
/**
49+
* Creates a successful result with the given value.
50+
*
51+
* @param value the computation result (may be {@code null})
52+
* @param <T> the value type
53+
* @return a success result wrapping the value
54+
*/
55+
@SuppressWarnings("unchecked")
56+
public static <T> AwaitResult<T> success(Object value) {
57+
return new AwaitResult<>((T) value, null, true);
58+
}
59+
60+
/**
61+
* Creates a failure result with the given exception.
62+
*
63+
* @param error the exception that caused the failure; must not be {@code null}
64+
* @param <T> the value type (never actually used, since the result is a failure)
65+
* @return a failure result wrapping the exception
66+
* @throws NullPointerException if {@code error} is {@code null}
67+
*/
68+
public static <T> AwaitResult<T> failure(Throwable error) {
69+
return new AwaitResult<>(null, Objects.requireNonNull(error), false);
70+
}
71+
72+
/** Returns {@code true} if this result represents a successful completion. */
73+
public boolean isSuccess() {
74+
return success;
75+
}
76+
77+
/** Returns {@code true} if this result represents a failed completion. */
78+
public boolean isFailure() {
79+
return !success;
80+
}
81+
82+
/**
83+
* Returns the value if successful.
84+
*
85+
* @return the computation result
86+
* @throws IllegalStateException if this result represents a failure
87+
*/
88+
public T getValue() {
89+
if (!success) throw new IllegalStateException("Cannot get value from a failed result");
90+
return value;
91+
}
92+
93+
/**
94+
* Returns the exception if failed.
95+
*
96+
* @return the exception that caused the failure
97+
* @throws IllegalStateException if this result represents a success
98+
*/
99+
public Throwable getError() {
100+
if (success) throw new IllegalStateException("Cannot get error from a successful result");
101+
return error;
102+
}
103+
104+
/**
105+
* Returns the value if successful, or applies the given function to
106+
* the error to produce a fallback value.
107+
*/
108+
public T getOrElse(Function<Throwable, ? extends T> fallback) {
109+
return success ? value : fallback.apply(error);
110+
}
111+
112+
/**
113+
* Returns a human-readable representation of this result:
114+
* {@code AwaitResult.Success[value]} or {@code AwaitResult.Failure[error]}.
115+
*/
116+
@Override
117+
public String toString() {
118+
return success
119+
? "AwaitResult.Success[" + value + "]"
120+
: "AwaitResult.Failure[" + error + "]";
121+
}
122+
}

0 commit comments

Comments
 (0)