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 ea300b74a0 Minor tweaks
ea300b74a0 is described below
commit ea300b74a008f0594917db81243cd445ada11132
Author: Daniel Sun <[email protected]>
AuthorDate: Sun Mar 8 03:37:10 2026 +0900
Minor tweaks
---
.../apache/groovy/parser/antlr4/AstBuilder.java | 8 ++++
.../groovy/parser/antlr4/ModifierManager.java | 3 +-
.../groovy/runtime/async/AsyncStreamGenerator.java | 24 ++++++++++
.../apache/groovy/runtime/async/GroovyPromise.java | 51 ++++++++++++++++++++
.../groovy/transform/AsyncAwaitSyntaxTest.groovy | 35 ++++++++++++++
.../groovy/transform/AsyncVirtualThreadTest.groovy | 55 ++++++++++++++--------
6 files changed, 155 insertions(+), 21 deletions(-)
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 3f221e35d8..55fd359bb0 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -1285,6 +1285,10 @@ public class AstBuilder extends
GroovyParserBaseVisitor<Object> {
throw createParsingFailedException("only sealed type declarations
should have `permits` clause", ctx);
}
+ if (modifierManager.containsAny(ASYNC)) {
+ throw createParsingFailedException("modifier `async` is not
allowed for type declarations", modifierManager.get(ASYNC).get());
+ }
+
int modifiers = modifierManager.getClassModifiersOpValue();
boolean syntheticPublic = ((modifiers & Opcodes.ACC_SYNTHETIC) != 0);
@@ -2013,6 +2017,10 @@ public class AstBuilder extends
GroovyParserBaseVisitor<Object> {
asBoolean(ctx.modifiers()) ?
this.visitModifiers(ctx.modifiers()) : Collections.emptyList()
);
+ if (modifierManager.containsAny(ASYNC)) {
+ throw createParsingFailedException("modifier `async` is not
allowed for variable declarations", modifierManager.get(ASYNC).get());
+ }
+
if (asBoolean(ctx.typeNamePairs())) { // e.g. def (int a, int b) = [1,
2]
return this.createMultiAssignmentDeclarationListStatement(ctx,
modifierManager);
}
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
index 4d86449b8d..729eb14cf2 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
@@ -37,6 +37,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.ABSTRACT;
+import static org.apache.groovy.parser.antlr4.GroovyLangParser.ASYNC;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.FINAL;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.NATIVE;
import static org.apache.groovy.parser.antlr4.GroovyLangParser.STATIC;
@@ -47,7 +48,7 @@ import static
org.apache.groovy.parser.antlr4.GroovyLangParser.VOLATILE;
*/
class ModifierManager {
private static final Map<Class, List<Integer>> INVALID_MODIFIERS_MAP =
Maps.of(
- ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT,
NATIVE),
+ ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT,
NATIVE, ASYNC),
MethodNode.class, Arrays.asList(VOLATILE/*, TRANSIENT*/) //
Transient is left open for properties for legacy reasons but should be removed
before ClassCompletionVerifier runs (CLASSGEN)
);
private AstBuilder astBuilder;
diff --git
a/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
b/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
index c3ba7176c3..1de114eb4f 100644
--- a/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
@@ -175,6 +175,21 @@ public class AsyncStreamGenerator<T> implements
AsyncStream<T> {
}
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Blocks the calling (consumer) thread on the {@link SynchronousQueue}
until
+ * the producer offers the next element, a completion sentinel, or an
error.
+ * If the stream has been {@linkplain #close() closed}, returns
+ * {@code Awaitable.of(false)} immediately without blocking.
+ * <p>
+ * The consumer thread is registered via {@code consumerThread} during the
+ * blocking call so that {@link #close()} can interrupt it if needed.
+ *
+ * @return an {@code Awaitable<Boolean>} that resolves to {@code true} if a
+ * new element is available via {@link #getCurrent()}, or {@code
false}
+ * if the stream is exhausted or closed
+ */
@Override
@SuppressWarnings("unchecked")
public Awaitable<Boolean> moveNext() {
@@ -208,6 +223,15 @@ public class AsyncStreamGenerator<T> implements
AsyncStream<T> {
}
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns the most recently consumed element. The value is updated each
time
+ * {@link #moveNext()} returns {@code true}.
+ *
+ * @return the current element, or {@code null} before the first successful
+ * {@code moveNext()} call
+ */
@Override
public T getCurrent() {
return current;
diff --git a/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
index df22416b2d..5d03000748 100644
--- a/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
+++ b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
@@ -47,6 +47,12 @@ public class GroovyPromise<T> implements Awaitable<T> {
private final CompletableFuture<T> future;
+ /**
+ * Creates a new {@code GroovyPromise} wrapping the given {@link
CompletableFuture}.
+ *
+ * @param future the backing future; must not be {@code null}
+ * @throws NullPointerException if {@code future} is {@code null}
+ */
public GroovyPromise(CompletableFuture<T> future) {
this.future = Objects.requireNonNull(future, "future must not be
null");
}
@@ -58,6 +64,13 @@ public class GroovyPromise<T> implements Awaitable<T> {
return new GroovyPromise<>(future);
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Waits if necessary for the computation to complete, then retrieves its
result.
+ * If the future was cancelled, the original {@link CancellationException}
is
+ * unwrapped from the JDK 23+ wrapper for cross-version consistency.
+ */
@Override
public T get() throws InterruptedException, ExecutionException {
try {
@@ -67,6 +80,12 @@ public class GroovyPromise<T> implements Awaitable<T> {
}
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Waits at most the given time for the computation to complete.
+ * Unwraps JDK 23+ {@link CancellationException} wrappers for consistency.
+ */
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException {
try {
@@ -76,6 +95,7 @@ public class GroovyPromise<T> implements Awaitable<T> {
}
}
+ /** {@inheritDoc} */
@Override
public boolean isDone() {
return future.isDone();
@@ -98,26 +118,47 @@ public class GroovyPromise<T> implements Awaitable<T> {
return future.cancel(true);
}
+ /** {@inheritDoc} */
@Override
public boolean isCancelled() {
return future.isCancelled();
}
+ /** {@inheritDoc} */
@Override
public boolean isCompletedExceptionally() {
return future.isCompletedExceptionally();
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns a new {@code GroovyPromise} whose result is obtained by applying
+ * the given function to this promise's result.
+ */
@Override
public <U> Awaitable<U> then(Function<? super T, ? extends U> fn) {
return new GroovyPromise<>(future.thenApply(fn));
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns a new {@code GroovyPromise} that is the result of composing this
+ * promise with the async function, enabling flat-mapping of awaitables.
+ */
@Override
public <U> Awaitable<U> thenCompose(Function<? super T, ? extends
Awaitable<U>> fn) {
return new GroovyPromise<>(future.thenCompose(t ->
fn.apply(t).toCompletableFuture()));
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns a new {@code GroovyPromise} that handles exceptions thrown by
this promise.
+ * The throwable passed to the handler is deeply unwrapped to strip JDK
+ * wrapper layers ({@code CompletionException}, {@code
ExecutionException}).
+ */
@Override
public Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn) {
return new GroovyPromise<>(future.exceptionally(t -> {
@@ -127,6 +168,11 @@ public class GroovyPromise<T> implements Awaitable<T> {
}));
}
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Returns the underlying {@link CompletableFuture} for interop with JDK
APIs.
+ */
@Override
public CompletableFuture<T> toCompletableFuture() {
return future;
@@ -143,6 +189,11 @@ public class GroovyPromise<T> implements Awaitable<T> {
return cause instanceof CancellationException ce ? ce : exception;
}
+ /**
+ * Returns a human-readable representation showing the promise state:
+ * {@code GroovyPromise{pending}}, {@code GroovyPromise{completed}}, or
+ * {@code GroovyPromise{failed}}.
+ */
@Override
public String toString() {
if (future.isDone()) {
diff --git
a/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
b/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
index 59e46116eb..249a4f317c 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
@@ -1677,6 +1677,41 @@ class AsyncAwaitSyntaxTest {
'''
}
+ @Test
+ void testAsyncOnClassDeclarationFails() {
+ shouldFail '''
+ async class Svc {}
+ '''
+ }
+
+ @Test
+ void testAsyncOnFieldDeclarationFails() {
+ shouldFail '''
+ class Svc {
+ async int x = 42
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncOnLocalVariableFails() {
+ shouldFail '''
+ def foo() {
+ async int x = 42
+ }
+ '''
+ }
+
+ @Test
+ void testAsyncOnConstructorFails() {
+ shouldFail '''
+ class Svc {
+ @groovy.transform.Async
+ Svc() {}
+ }
+ '''
+ }
+
// =====================================================================
// 7. 'yield return' — async generator (C#-style async streams)
// =====================================================================
diff --git
a/src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy
b/src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy
index 50d24a8ebe..a9926ff4d0 100644
---
a/src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy
+++
b/src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy
@@ -146,15 +146,17 @@ final class AsyncVirtualThreadTest {
assertScript '''
import groovy.concurrent.Awaitable
import java.util.concurrent.Executors
+ import java.util.concurrent.ExecutorService
import java.util.concurrent.atomic.AtomicReference
def savedExecutor = Awaitable.getExecutor()
+ ExecutorService customPool = Executors.newFixedThreadPool(2, { r ->
+ def t = new Thread(r)
+ t.setName("custom-async-" + t.getId())
+ t.setDaemon(true)
+ t
+ })
try {
- def customPool = Executors.newFixedThreadPool(2, { r ->
- def t = new Thread(r)
- t.setName("custom-async-" + t.getId())
- t
- })
Awaitable.setExecutor(customPool)
def asyncName = async {
@@ -165,6 +167,7 @@ final class AsyncVirtualThreadTest {
assert threadName.startsWith("custom-async-")
} finally {
Awaitable.setExecutor(savedExecutor)
+ customPool.shutdownNow()
}
'''
}
@@ -174,20 +177,26 @@ final class AsyncVirtualThreadTest {
assertScript '''
import groovy.concurrent.Awaitable
import java.util.concurrent.Executors
+ import java.util.concurrent.ExecutorService
def originalExecutor = Awaitable.getExecutor()
// Set a custom executor
- Awaitable.setExecutor(Executors.newSingleThreadExecutor())
- assert Awaitable.getExecutor() != originalExecutor
- // Reset to null — should restore default
- Awaitable.setExecutor(null)
- def restored = Awaitable.getExecutor()
- assert restored != null
- // Verify it works
- def task = async { 42 }; def awaitable = task()
- assert await(awaitable) == 42
- // Restore original
- Awaitable.setExecutor(originalExecutor)
+ ExecutorService tempPool = Executors.newSingleThreadExecutor()
+ try {
+ Awaitable.setExecutor(tempPool)
+ assert Awaitable.getExecutor() != originalExecutor
+ // Reset to null — should restore default
+ Awaitable.setExecutor(null)
+ def restored = Awaitable.getExecutor()
+ assert restored != null
+ // Verify it works
+ def task = async { 42 }; def awaitable = task()
+ assert await(awaitable) == 42
+ // Restore original
+ Awaitable.setExecutor(originalExecutor)
+ } finally {
+ tempPool.shutdownNow()
+ }
'''
}
@@ -196,12 +205,14 @@ final class AsyncVirtualThreadTest {
assertScript '''
import groovy.transform.Async
import java.util.concurrent.Executor
+ import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CustomExecutorService {
- static Executor myPool = Executors.newFixedThreadPool(1, { r ->
+ static ExecutorService myPool =
Executors.newFixedThreadPool(1, { r ->
def t = new Thread(r)
t.setName("my-pool-thread")
+ t.setDaemon(true)
t
})
@@ -211,9 +222,13 @@ final class AsyncVirtualThreadTest {
}
}
- def svc = new CustomExecutorService()
- def result = svc.doWork().get()
- assert result.startsWith("my-pool-thread")
+ try {
+ def svc = new CustomExecutorService()
+ def result = svc.doWork().get()
+ assert result.startsWith("my-pool-thread")
+ } finally {
+ CustomExecutorService.myPool.shutdownNow()
+ }
'''
}