diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 3eb0dedeede..2cb4526fbf7 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -646,6 +646,7 @@ statement
| tryCatchStatement #tryCatchStmtAlt
| SYNCHRONIZED expressionInPar nls block #synchronizedStmtAlt
| RETURN expression? #returnStmtAlt
+ | RETURN AT identifier expression? #returnAtStmtAlt
| THROW expression #throwStmtAlt
| breakStatement #breakStmtAlt
| continueStatement #continueStmtAlt
diff --git a/src/main/java/groovy/transform/SupportsLoopControl.java b/src/main/java/groovy/transform/SupportsLoopControl.java
new file mode 100644
index 00000000000..3fdc31f2194
--- /dev/null
+++ b/src/main/java/groovy/transform/SupportsLoopControl.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.transform;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks an iterator-style method as cooperating with the loop-control protocol
+ * (GROOVY-12126): {@code break} and {@code continue} may be used inside a
+ * closure passed directly as an argument to the method, with these semantics:
+ *
+ * - value-ignoring iterators (e.g. {@code each}, {@code times}, {@code upto}):
+ * {@code break} stops the iteration, {@code continue} proceeds to the next
+ * element;
+ * - value-consuming iterators (e.g. {@code collect}, {@code findAll},
+ * {@code inject}): {@code break} stops the iteration excluding the current
+ * element's contribution, {@code continue} skips the current element's
+ * contribution.
+ *
+ * Cooperating methods catch the compiler-internal
+ * {@link org.codehaus.groovy.runtime.LoopControl} signals around each
+ * per-element closure invocation. A signal thrown inside a closure passed to a
+ * method without this annotation propagates to the caller.
+ *
+ * @since 6.0.0
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface SupportsLoopControl {
+}
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 aaeb90b9fd7..fa9d5c960fc 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -913,6 +913,20 @@ public ReturnStatement visitReturnStmtAlt(final ReturnStmtAltContext ctx) {
ctx);
}
+ @Override
+ public ReturnStatement visitReturnAtStmtAlt(final ReturnAtStmtAltContext ctx) {
+ if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
+ throw createParsingFailedException("switch expression does not support `return`", ctx);
+ }
+
+ ReturnStatement returnStatement = new ReturnStatement(asBoolean(ctx.expression())
+ ? (Expression) this.visit(ctx.expression())
+ : ConstantExpression.EMPTY_EXPRESSION);
+ returnStatement.setTarget(this.visitIdentifier(ctx.identifier()));
+
+ return configureAST(returnStatement, ctx);
+ }
+
@Override
public ThrowStatement visitThrowStmtAlt(final ThrowStmtAltContext ctx) {
return configureAST(
diff --git a/src/main/java/org/codehaus/groovy/ast/stmt/ReturnStatement.java b/src/main/java/org/codehaus/groovy/ast/stmt/ReturnStatement.java
index 4320e12fdfe..e9fefefd2b3 100644
--- a/src/main/java/org/codehaus/groovy/ast/stmt/ReturnStatement.java
+++ b/src/main/java/org/codehaus/groovy/ast/stmt/ReturnStatement.java
@@ -42,6 +42,14 @@ public class ReturnStatement extends Statement {
private Expression expression;
+ /**
+ * The optional non-local return target, i.e. the {@code name} in {@code return@name expr}.
+ * Null for an ordinary return.
+ *
+ * @since 6.0.0
+ */
+ private String target;
+
/**
* Constructs a return statement from an expression statement by extracting its expression.
*
@@ -79,9 +87,29 @@ public void setExpression(final Expression expression) {
this.expression = expression;
}
+ /**
+ * Returns the non-local return target for a {@code return@name expr} statement.
+ *
+ * @return the name of the enclosing method targeted by this return, or null for an ordinary return
+ * @since 6.0.0
+ */
+ public String getTarget() {
+ return target;
+ }
+
+ /**
+ * Sets the non-local return target, i.e. the {@code name} in {@code return@name expr}.
+ *
+ * @param target the name of the enclosing method targeted by this return
+ * @since 6.0.0
+ */
+ public void setTarget(final String target) {
+ this.target = target;
+ }
+
@Override
public String getText() {
- return "return " + expression.getText();
+ return "return" + (target != null ? "@" + target : "") + " " + expression.getText();
}
/**
diff --git a/src/main/java/org/codehaus/groovy/control/CompilationUnit.java b/src/main/java/org/codehaus/groovy/control/CompilationUnit.java
index 2a5db311b7a..06edc60da39 100644
--- a/src/main/java/org/codehaus/groovy/control/CompilationUnit.java
+++ b/src/main/java/org/codehaus/groovy/control/CompilationUnit.java
@@ -342,6 +342,10 @@ private void addPhaseOperations() {
visitor.visitClass(classNode);
}, Phases.CANONICALIZATION);
+ addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
+ new NonLocalControlFlowRewriter(source).rewrite(classNode);
+ }, Phases.CANONICALIZATION);
+
addPhaseOperation((final SourceUnit source, final GeneratorContext context, final ClassNode classNode) -> {
Object callback = classNode.getNodeMetaData(DYNAMIC_OUTER_NODE_CALLBACK);
if (callback instanceof IPrimaryClassNodeOperation) {
diff --git a/src/main/java/org/codehaus/groovy/control/NonLocalControlFlowRewriter.java b/src/main/java/org/codehaus/groovy/control/NonLocalControlFlowRewriter.java
new file mode 100644
index 00000000000..73a7ac0f93e
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/control/NonLocalControlFlowRewriter.java
@@ -0,0 +1,386 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.control;
+
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
+import org.codehaus.groovy.ast.InnerClassNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.TupleExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.BreakStatement;
+import org.codehaus.groovy.ast.stmt.CaseStatement;
+import org.codehaus.groovy.ast.stmt.CatchStatement;
+import org.codehaus.groovy.ast.stmt.ContinueStatement;
+import org.codehaus.groovy.ast.stmt.DoWhileStatement;
+import org.codehaus.groovy.ast.stmt.EmptyStatement;
+import org.codehaus.groovy.ast.stmt.ForStatement;
+import org.codehaus.groovy.ast.stmt.IfStatement;
+import org.codehaus.groovy.ast.stmt.ReturnStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.SwitchStatement;
+import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
+import org.codehaus.groovy.ast.stmt.TryCatchStatement;
+import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.classgen.VariableScopeVisitor;
+import org.codehaus.groovy.runtime.LoopControl;
+import org.codehaus.groovy.runtime.NonLocalReturn;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Set;
+
+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.castX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.catchS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.notX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.tryCatchS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+
+/**
+ * Desugars non-local control flow from closures (GROOVY-12126).
+ *
+ * A {@code return@name expr} inside a closure whose lexically enclosing method
+ * is named {@code name} (or {@code return@script} inside a script body) becomes
+ * a call to {@link NonLocalReturn#raise}, throwing a token-matched signal; the
+ * target method's body is wrapped so the signal is caught and turned into an
+ * ordinary return of the carried value. Methods without such returns are left
+ * untouched. The reserved target {@code script} refers to the script body.
+ *
+ * A {@code break} or {@code continue} inside a closure passed directly as a
+ * method call argument — and not bound to a loop (or switch, for break) within
+ * the closure — becomes {@code throw LoopControl.BREAK/CONTINUE}; cooperating
+ * iterator methods (marked {@link groovy.transform.SupportsLoopControl}) catch
+ * the signal per element. Such closures are tagged with node metadata under
+ * the {@code NonLocalControlFlowRewriter.class} key so the static type checker
+ * can verify the callee cooperates.
+ *
+ * Runs at {@link Phases#CANONICALIZATION}, before static type checking and
+ * class generation, so the desugared form compiles identically under dynamic
+ * and static compilation.
+ */
+public class NonLocalControlFlowRewriter extends ClassCodeVisitorSupport {
+
+ private static final ClassNode NLR_TYPE = ClassHelper.make(NonLocalReturn.class);
+ private static final ClassNode LOOP_CONTROL_TYPE = ClassHelper.make(LoopControl.class);
+ private static final String TOKEN_NAME = "$nlr$token";
+ private static final String SCRIPT_TARGET = "script";
+
+ private final SourceUnit sourceUnit;
+
+ private ClassNode currentClass;
+ private MethodNode currentMethod;
+ private int closureDepth;
+ private boolean methodNeedsWrapper;
+ private boolean classNeedsScopeRepair;
+
+ /** Closures passed directly as method call arguments, i.e. eligible for break/continue. */
+ private final Set eligibleClosures = Collections.newSetFromMap(new IdentityHashMap<>());
+ private ClosureExpression currentClosure;
+ private int loopDepth;
+ private int switchDepth;
+
+ public NonLocalControlFlowRewriter(final SourceUnit sourceUnit) {
+ this.sourceUnit = sourceUnit;
+ }
+
+ @Override
+ protected SourceUnit getSourceUnit() {
+ return sourceUnit;
+ }
+
+ public void rewrite(final ClassNode classNode) {
+ currentClass = classNode;
+ classNeedsScopeRepair = false;
+ visitClass(classNode);
+ if (classNeedsScopeRepair) {
+ // the token variable is declared in the method body and referenced
+ // inside closures, so captured-variable bookkeeping must be redone
+ ClassNode outermost = classNode;
+ while (outermost instanceof InnerClassNode && ((InnerClassNode) outermost).isAnonymous()) {
+ outermost = outermost.getOuterClass();
+ }
+ new VariableScopeVisitor(sourceUnit).visitClass(outermost);
+ }
+ }
+
+ @Override
+ protected void visitConstructorOrMethod(final MethodNode node, final boolean isConstructor) {
+ MethodNode previousMethod = currentMethod;
+ int previousDepth = closureDepth;
+ boolean previousNeedsWrapper = methodNeedsWrapper;
+ currentMethod = isConstructor ? null : node;
+ closureDepth = 0;
+ methodNeedsWrapper = false;
+ try {
+ super.visitConstructorOrMethod(node, isConstructor);
+ if (methodNeedsWrapper) {
+ wrapMethod(node);
+ classNeedsScopeRepair = true;
+ }
+ } finally {
+ currentMethod = previousMethod;
+ closureDepth = previousDepth;
+ methodNeedsWrapper = previousNeedsWrapper;
+ }
+ }
+
+ @Override
+ public void visitClosureExpression(final ClosureExpression expression) {
+ ClosureExpression previousClosure = currentClosure;
+ int previousLoopDepth = loopDepth;
+ int previousSwitchDepth = switchDepth;
+ closureDepth += 1;
+ currentClosure = expression;
+ loopDepth = 0;
+ switchDepth = 0;
+ try {
+ super.visitClosureExpression(expression);
+ } finally {
+ closureDepth -= 1;
+ currentClosure = previousClosure;
+ loopDepth = previousLoopDepth;
+ switchDepth = previousSwitchDepth;
+ }
+ }
+
+ @Override
+ public void visitMethodCallExpression(final MethodCallExpression call) {
+ markEligibleClosureArguments(call.getArguments());
+ super.visitMethodCallExpression(call);
+ }
+
+ @Override
+ public void visitStaticMethodCallExpression(final StaticMethodCallExpression call) {
+ markEligibleClosureArguments(call.getArguments());
+ super.visitStaticMethodCallExpression(call);
+ }
+
+ private void markEligibleClosureArguments(final Expression arguments) {
+ if (arguments instanceof TupleExpression) {
+ for (Expression argument : ((TupleExpression) arguments).getExpressions()) {
+ if (argument instanceof ClosureExpression) {
+ eligibleClosures.add((ClosureExpression) argument);
+ }
+ }
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ // break/continue in closure arguments: statement-slot replacement
+
+ /**
+ * Returns the loop-control signal throw replacing the given statement, or
+ * null when the statement is not an applicable break/continue (reporting
+ * a compile error for break/continue misuses inside closures).
+ */
+ private Statement replacementFor(final Statement statement) {
+ boolean isBreak = statement instanceof BreakStatement;
+ if (!isBreak && !(statement instanceof ContinueStatement)) return null;
+ if (closureDepth == 0) return null; // LabelVerifier's domain
+ if (loopDepth > 0 || (isBreak && switchDepth > 0)) return null; // bound to a real loop/switch in the closure
+
+ String kind = isBreak ? "break" : "continue";
+ String label = isBreak ? ((BreakStatement) statement).getLabel() : ((ContinueStatement) statement).getLabel();
+ if (label != null) {
+ addError("labeled " + kind + " is not allowed inside a closure", statement);
+ return null;
+ }
+ if (!eligibleClosures.contains(currentClosure)) {
+ addError(kind + " inside a closure is only allowed when the closure is a direct argument of a method call", statement);
+ return null;
+ }
+ currentClosure.putNodeMetaData(NonLocalControlFlowRewriter.class, Boolean.TRUE);
+ Statement replacement = throwS(propX(classX(LOOP_CONTROL_TYPE), isBreak ? "BREAK" : "CONTINUE"));
+ replacement.setSourcePosition(statement);
+ return replacement;
+ }
+
+ @Override
+ public void visitBlockStatement(final BlockStatement block) {
+ List statements = block.getStatements();
+ for (int i = 0, n = statements.size(); i < n; i += 1) {
+ Statement replacement = replacementFor(statements.get(i));
+ if (replacement != null) statements.set(i, replacement);
+ }
+ super.visitBlockStatement(block);
+ }
+
+ @Override
+ public void visitIfElse(final IfStatement ifElse) {
+ Statement replacement = replacementFor(ifElse.getIfBlock());
+ if (replacement != null) ifElse.setIfBlock(replacement);
+ replacement = replacementFor(ifElse.getElseBlock());
+ if (replacement != null) ifElse.setElseBlock(replacement);
+ super.visitIfElse(ifElse);
+ }
+
+ @Override
+ public void visitForLoop(final ForStatement forLoop) {
+ loopDepth += 1;
+ try {
+ super.visitForLoop(forLoop);
+ } finally {
+ loopDepth -= 1;
+ }
+ }
+
+ @Override
+ public void visitWhileLoop(final WhileStatement loop) {
+ loopDepth += 1;
+ try {
+ super.visitWhileLoop(loop);
+ } finally {
+ loopDepth -= 1;
+ }
+ }
+
+ @Override
+ public void visitDoWhileLoop(final DoWhileStatement loop) {
+ loopDepth += 1;
+ try {
+ super.visitDoWhileLoop(loop);
+ } finally {
+ loopDepth -= 1;
+ }
+ }
+
+ @Override
+ public void visitSwitch(final SwitchStatement statement) {
+ switchDepth += 1;
+ try {
+ Statement replacement = replacementFor(statement.getDefaultStatement());
+ if (replacement != null) statement.setDefaultStatement(replacement);
+ super.visitSwitch(statement);
+ } finally {
+ switchDepth -= 1;
+ }
+ }
+
+ @Override
+ public void visitCaseStatement(final CaseStatement statement) {
+ Statement replacement = replacementFor(statement.getCode());
+ if (replacement != null) statement.setCode(replacement);
+ super.visitCaseStatement(statement);
+ }
+
+ @Override
+ public void visitTryCatchFinally(final TryCatchStatement statement) {
+ Statement replacement = replacementFor(statement.getTryStatement());
+ if (replacement != null) statement.setTryStatement(replacement);
+ for (CatchStatement catchStatement : statement.getCatchStatements()) {
+ replacement = replacementFor(catchStatement.getCode());
+ if (replacement != null) catchStatement.setCode(replacement);
+ }
+ replacement = replacementFor(statement.getFinallyStatement());
+ if (replacement != null) statement.setFinallyStatement(replacement);
+ super.visitTryCatchFinally(statement);
+ }
+
+ @Override
+ public void visitSynchronizedStatement(final SynchronizedStatement statement) {
+ Statement replacement = replacementFor(statement.getCode());
+ if (replacement != null) statement.setCode(replacement);
+ super.visitSynchronizedStatement(statement);
+ }
+
+ @Override
+ public void visitReturnStatement(final ReturnStatement statement) {
+ super.visitReturnStatement(statement);
+ String target = statement.getTarget();
+ if (target == null) return;
+
+ if (currentMethod == null) {
+ addError("return@" + target + " is only allowed inside a method body (or a closure within one)", statement);
+ return;
+ }
+ if (!isTarget(currentMethod, target)) {
+ if (isOuterTarget(target)) {
+ addError("return@" + target + ": cannot return across a class boundary; '" + target
+ + "' lexically encloses class '" + currentClass.getNameWithoutPackage()
+ + "' but is not one of its methods", statement);
+ } else {
+ String hint = currentClass.isScript() && currentMethod.isScriptBody()
+ ? "; use return@script to return from a script body" : "";
+ addError("return@" + target + ": no lexically enclosing method named '" + target + "'" + hint, statement);
+ }
+ return;
+ }
+
+ statement.setTarget(null);
+ if (closureDepth == 0) return; // plain return suffices at method level
+
+ Expression value = statement.getExpression() == ConstantExpression.EMPTY_EXPRESSION
+ ? nullX() : statement.getExpression();
+ Expression raise = callX(NLR_TYPE, "raise", args(varX(TOKEN_NAME, ClassHelper.OBJECT_TYPE), value));
+ raise.setSourcePosition(statement);
+ statement.setExpression(raise);
+ methodNeedsWrapper = true;
+ }
+
+ private static boolean isTarget(final MethodNode method, final String target) {
+ return target.equals(method.getName()) || (SCRIPT_TARGET.equals(target) && method.isScriptBody());
+ }
+
+ private boolean isOuterTarget(final String target) {
+ ClassNode cn = currentClass;
+ while (cn != null) {
+ MethodNode enclosing = cn.getEnclosingMethod();
+ if (enclosing != null && isTarget(enclosing, target)) return true;
+ cn = cn.getOuterClass();
+ }
+ return false;
+ }
+
+ private static void wrapMethod(final MethodNode method) {
+ Parameter signal = new Parameter(NLR_TYPE, "$nlr$e");
+ ClassNode returnType = method.getReturnType();
+ Statement returnCarriedValue = ClassHelper.isPrimitiveVoid(returnType)
+ ? new ReturnStatement(ConstantExpression.EMPTY_EXPRESSION)
+ : returnS(castX(returnType, callX(varX(signal), "getValue")));
+ Statement catchBody = block(
+ ifS(notX(callX(varX(signal), "matches", args(varX(TOKEN_NAME, ClassHelper.OBJECT_TYPE)))), throwS(varX(signal))),
+ returnCarriedValue);
+ TryCatchStatement handler = tryCatchS(method.getCode(), EmptyStatement.INSTANCE, catchS(signal, catchBody));
+ BlockStatement newCode = block(
+ declS(localVarX(TOKEN_NAME, ClassHelper.OBJECT_TYPE), ctorX(ClassHelper.OBJECT_TYPE)),
+ handler);
+ newCode.setSourcePosition(method.getCode());
+ method.setCode(newCode);
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
index 8c3a14acac2..a0d5e435ea4 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
@@ -43,6 +43,7 @@
import groovy.lang.Range;
import groovy.lang.SpreadMap;
import groovy.lang.Tuple2;
+import groovy.transform.SupportsLoopControl;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.FirstParam;
import groovy.transform.stc.FromString;
@@ -242,6 +243,17 @@ public int compare(Map.Entry e1, Map.Entry e2) {
private static final NumberAwareComparator COMPARABLE_NUMBER_AWARE_COMPARATOR = new NumberAwareComparator<>();
+ // internal helper method: calls the closure translating loop-control signals;
+ // returns false if the iteration should stop (LoopControl.BREAK), true otherwise
+ private static boolean callResumable(Closure> closure, Object arg) {
+ try {
+ closure.call(arg);
+ return true;
+ } catch (LoopControl signal) {
+ return signal != LoopControl.BREAK;
+ }
+ }
+
// internal helper method
protected static T callClosureForLine(@ClosureParams(value=FromString.class, options={"String","String,Integer"}) Closure closure, String line, int counter) {
if (closure.getMaximumNumberOfParameters() == 2) {
@@ -2438,6 +2450,7 @@ public static Collection collect(Object self) {
* @return a List of the transformed values
* @since 1.0
*/
+ @SupportsLoopControl
public static List collect(Object self, Closure transform) {
return collect(self, new ArrayList<>(), transform);
}
@@ -2452,6 +2465,7 @@ public static List collect(Object self, Closure transform) {
* @return the collector with all transformed values added to it
* @since 1.0
*/
+ @SupportsLoopControl
public static > C collect(Object self, C collector, Closure extends T> transform) {
return collect(InvokerHelper.asIterator(self), collector, transform);
}
@@ -2465,6 +2479,7 @@ public static > C collect(Object self, C collector, C
* @return a List of the transformed values
* @since 2.5.0
*/
+ @SupportsLoopControl
public static List collect(Iterator self, @ClosureParams(FirstParam.FirstGenericType.class) Closure transform) {
return collect(self, new ArrayList<>(), transform);
}
@@ -2619,9 +2634,17 @@ public void remove() {
* @return the collector with all transformed values added to it
* @since 2.5.0
*/
+ @SupportsLoopControl
public static > C collect(Iterator self, C collector, @ClosureParams(FirstParam.FirstGenericType.class) Closure extends T> transform) {
while (self.hasNext()) {
- collector.add(transform.call(self.next()));
+ T transformed;
+ try {
+ transformed = transform.call(self.next());
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ continue;
+ }
+ collector.add(transformed);
}
return collector;
}
@@ -2667,6 +2690,7 @@ public static List collect(Iterable self) {
* @return a List of the transformed values
* @since 2.5.0
*/
+ @SupportsLoopControl
public static List collect(Iterable self, @ClosureParams(FirstParam.FirstGenericType.class) Closure transform) {
return collect(self.iterator(), transform);
}
@@ -2699,9 +2723,17 @@ public static List collect(Iterable self, Function super E, ? ext
* @return the collector with all transformed values added to it
* @since 2.5.0
*/
+ @SupportsLoopControl
public static > C collect(Iterable self, C collector, @ClosureParams(FirstParam.FirstGenericType.class) Closure extends T> transform) {
for (E element : self) {
- collector.add(transform.call(element));
+ T transformed;
+ try {
+ transformed = transform.call(element);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ continue;
+ }
+ collector.add(transformed);
if (transform.getDirective() == Closure.DONE) {
break;
}
@@ -2774,9 +2806,17 @@ public static List collect(Iterable self, BiFunction super E,
* @return the collector with all transformed values added to it
* @since 1.0
*/
+ @SupportsLoopControl
public static > C collect(Map self, C collector, @ClosureParams(MapEntryOrKeyValue.class) Closure extends T> transform) {
for (Map.Entry entry : self.entrySet()) {
- collector.add(callClosureForMapEntry(transform, entry));
+ T transformed;
+ try {
+ transformed = callClosureForMapEntry(transform, entry);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ continue;
+ }
+ collector.add(transformed);
}
return collector;
}
@@ -2795,6 +2835,7 @@ public static > C collect(Map self, C col
* @return the resultant list of transformed values
* @since 1.0
*/
+ @SupportsLoopControl
public static List collect(Map self, @ClosureParams(MapEntryOrKeyValue.class) Closure transform) {
return collect(self, new ArrayList<>(self.size()), transform);
}
@@ -4257,12 +4298,13 @@ public static Number div(Character left, Character right) {
* @param closure the closure to call
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(Number self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
int self1 = self.intValue();
int to1 = to.intValue();
if (self1 >= to1) {
for (int i = self1; i >= to1; i--) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4278,11 +4320,12 @@ public static void downto(Number self, Number to, @ClosureParams(FirstParam.clas
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(long self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
long to1 = to.longValue();
if (self >= to1) {
for (long i = self; i >= to1; i--) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4298,11 +4341,12 @@ public static void downto(long self, Number to, @ClosureParams(FirstParam.class)
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(Long self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
long to1 = to.longValue();
if (self >= to1) {
for (long i = self; i >= to1; i--) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4318,11 +4362,12 @@ public static void downto(Long self, Number to, @ClosureParams(FirstParam.class)
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(float self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
float to1 = to.floatValue();
if (self >= to1) {
for (float i = self; i >= to1; i--) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4337,11 +4382,12 @@ public static void downto(float self, Number to, @ClosureParams(FirstParam.class
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(Float self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
float to1 = to.floatValue();
if (self >= to1) {
for (float i = self; i >= to1; i--) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4356,11 +4402,12 @@ public static void downto(Float self, Number to, @ClosureParams(FirstParam.class
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(double self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
double to1 = to.doubleValue();
if (self >= to1) {
for (double i = self; i >= to1; i--) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4375,11 +4422,12 @@ public static void downto(double self, Number to, @ClosureParams(FirstParam.clas
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(Double self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
double to1 = to.doubleValue();
if (self >= to1) {
for (double i = self; i >= to1; i--) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4394,6 +4442,7 @@ public static void downto(Double self, Number to, @ClosureParams(FirstParam.clas
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(BigInteger self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
if (to instanceof BigDecimal to1) {
final BigDecimal one = BigDecimal.valueOf(10, 1); // That's what you get for "1.0".
@@ -4411,7 +4460,7 @@ public static void downto(BigInteger self, Number to, @ClosureParams(FirstParam.
final BigInteger one = BigInteger.valueOf(1);
if (self.compareTo(to1) >= 0) {
for (BigInteger i = self; i.compareTo(to1) >= 0; i = i.subtract(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException(
@@ -4423,7 +4472,7 @@ public static void downto(BigInteger self, Number to, @ClosureParams(FirstParam.
final BigInteger to1 = new BigInteger(to.toString());
if (self.compareTo(to1) >= 0) {
for (BigInteger i = self; i.compareTo(to1) >= 0; i = i.subtract(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException(
@@ -4448,12 +4497,13 @@ public static void downto(BigInteger self, Number to, @ClosureParams(FirstParam.
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void downto(BigDecimal self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
final BigDecimal one = BigDecimal.valueOf(10, 1); // Quick way to get "1.0".
if (to instanceof BigDecimal to1) {
if (self.compareTo(to1) >= 0) {
for (BigDecimal i = self; i.compareTo(to1) >= 0; i = i.subtract(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else {
throw new GroovyRuntimeException("The argument (" + to +
@@ -4463,7 +4513,7 @@ public static void downto(BigDecimal self, Number to, @ClosureParams(FirstParam.
BigDecimal to1 = new BigDecimal((BigInteger) to);
if (self.compareTo(to1) >= 0) {
for (BigDecimal i = self; i.compareTo(to1) >= 0; i = i.subtract(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -4471,7 +4521,7 @@ public static void downto(BigDecimal self, Number to, @ClosureParams(FirstParam.
BigDecimal to1 = NumberMath.toBigDecimal(to);
if (self.compareTo(to1) >= 0) {
for (BigDecimal i = self; i.compareTo(to1) >= 0; i = i.subtract(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -5068,6 +5118,7 @@ public static String dump(Object self) {
* @return the self Object
* @since 1.0
*/
+ @SupportsLoopControl
public static T each(T self, @ClosureParams(value=FromString.class, options="?") Closure closure) {
each(InvokerHelper.asIterator(self), closure);
return self;
@@ -5080,6 +5131,7 @@ public static T each(T self, @ClosureParams(value=FromString.class, options=
* @param closure the closure applied on each element found
* @return the self Iterable
*/
+ @SupportsLoopControl
public static Iterable each(Iterable self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
each(self.iterator(), closure);
return self;
@@ -5114,10 +5166,11 @@ public static Iterable each(Iterable self, Consumer super T> consume
* @return the (now exhausted) self Iterator
* @since 2.4.0
*/
+ @SupportsLoopControl
public static Iterator each(Iterator self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
while (self.hasNext()) {
Object arg = self.next();
- closure.call(arg);
+ if (!callResumable(closure, arg)) break;
}
return self;
}
@@ -5130,6 +5183,7 @@ public static Iterator each(Iterator self, @ClosureParams(FirstParam.F
* @return the self Collection
* @since 2.4.0
*/
+ @SupportsLoopControl
public static Collection each(Collection self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
return (Collection) each((Iterable) self, closure);
}
@@ -5142,6 +5196,7 @@ public static Collection each(Collection self, @ClosureParams(FirstPar
* @return the self List
* @since 2.4.0
*/
+ @SupportsLoopControl
public static List each(List self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
return (List) each((Iterable) self, closure);
}
@@ -5154,6 +5209,7 @@ public static List each(List self, @ClosureParams(FirstParam.FirstGene
* @return the self Set
* @since 2.4.0
*/
+ @SupportsLoopControl
public static Set each(Set self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
return (Set) each((Iterable) self, closure);
}
@@ -5166,6 +5222,7 @@ public static Set each(Set self, @ClosureParams(FirstParam.FirstGeneri
* @return the self SortedSet
* @since 2.4.0
*/
+ @SupportsLoopControl
public static SortedSet each(SortedSet self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
return (SortedSet) each((Iterable) self, closure);
}
@@ -5192,9 +5249,14 @@ public static SortedSet each(SortedSet self, @ClosureParams(FirstParam
* @return returns the self parameter
* @since 1.5.0
*/
+ @SupportsLoopControl
public static Map each(Map self, @ClosureParams(MapEntryOrKeyValue.class) Closure> closure) {
for (Map.Entry entry : self.entrySet()) {
- callClosureForMapEntry(closure, entry);
+ try {
+ callClosureForMapEntry(closure, entry);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ }
}
return self;
}
@@ -5260,6 +5322,7 @@ public static Iterator> eachPermutation(Iterable self, Closure cl
* @return the self Object
* @since 1.0
*/
+ @SupportsLoopControl
public static T eachWithIndex(T self, @ClosureParams(value=FromString.class, options="?,Integer") Closure closure) {
final Object[] args = new Object[2];
int counter = 0;
@@ -5281,6 +5344,7 @@ public static T eachWithIndex(T self, @ClosureParams(value=FromString.class,
* @return the self Iterable
* @since 2.3.0
*/
+ @SupportsLoopControl
public static Iterable eachWithIndex(Iterable self, @ClosureParams(value=FromString.class, options="T,Integer") Closure closure) {
eachWithIndex(self.iterator(), closure);
return self;
@@ -5319,13 +5383,18 @@ public static Iterable eachWithIndex(Iterable self, ObjIntConsumer s
* @return the (now exhausted) self Iterator
* @since 2.3.0
*/
+ @SupportsLoopControl
public static Iterator eachWithIndex(Iterator self, @ClosureParams(value=FromString.class, options="T,Integer") Closure closure) {
final Object[] args = new Object[2];
int counter = 0;
while (self.hasNext()) {
args[0] = self.next();
args[1] = counter++;
- closure.call(args);
+ try {
+ closure.call(args);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ }
}
return self;
}
@@ -5340,6 +5409,7 @@ public static Iterator eachWithIndex(Iterator self, @ClosureParams(val
* @return the self Collection
* @since 2.4.0
*/
+ @SupportsLoopControl
public static Collection eachWithIndex(Collection self, @ClosureParams(value=FromString.class, options="T,Integer") Closure closure) {
return (Collection) eachWithIndex((Iterable) self, closure);
}
@@ -5354,6 +5424,7 @@ public static Collection eachWithIndex(Collection self, @ClosureParams
* @return the self List
* @since 2.4.0
*/
+ @SupportsLoopControl
public static List eachWithIndex(List self, @ClosureParams(value=FromString.class, options="T,Integer") Closure closure) {
return (List) eachWithIndex((Iterable) self, closure);
}
@@ -5368,6 +5439,7 @@ public static List eachWithIndex(List self, @ClosureParams(value=FromS
* @return the self Set
* @since 2.4.0
*/
+ @SupportsLoopControl
public static Set eachWithIndex(Set self, @ClosureParams(value=FromString.class, options="T,Integer") Closure closure) {
return (Set) eachWithIndex((Iterable) self, closure);
}
@@ -5382,6 +5454,7 @@ public static Set eachWithIndex(Set self, @ClosureParams(value=FromStr
* @return the self SortedSet
* @since 2.4.0
*/
+ @SupportsLoopControl
public static SortedSet eachWithIndex(SortedSet self, @ClosureParams(value=FromString.class, options="T,Integer") Closure closure) {
return (SortedSet) eachWithIndex((Iterable) self, closure);
}
@@ -5404,10 +5477,15 @@ public static SortedSet eachWithIndex(SortedSet self, @ClosureParams(v
* @return the self Object
* @since 1.5.0
*/
+ @SupportsLoopControl
public static Map eachWithIndex(Map self, @ClosureParams(value=MapEntryOrKeyValue.class, options="index=true") Closure> closure) {
int counter = 0;
for (Map.Entry entry : self.entrySet()) {
- callClosureForMapEntryAndCounter(closure, entry, counter++);
+ try {
+ callClosureForMapEntryAndCounter(closure, entry, counter++);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ }
}
return self;
}
@@ -6037,11 +6115,19 @@ public static Map.Entry find(Map self, @ClosureParams(MapEntr
* @return a new subMap
* @since 1.0
*/
+ @SupportsLoopControl
public static Map findAll(Map self, @ClosureParams(MapEntryOrKeyValue.class) Closure closure) {
Map answer = createSimilarMap(self);
BooleanClosureWrapper bcw = new BooleanClosureWrapper(closure);
for (Map.Entry entry : self.entrySet()) {
- if (bcw.callForMap(entry)) {
+ boolean keep;
+ try {
+ keep = bcw.callForMap(entry);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ continue;
+ }
+ if (keep) {
answer.put(entry.getKey(), entry.getValue());
}
}
@@ -6084,6 +6170,7 @@ public static Map findAll(Map self, BiPredicate super K, ?
* @return a Set of matching values
* @since 2.4.0
*/
+ @SupportsLoopControl
public static Set findAll(Set self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
return (Set) findAll((Collection) self, closure);
}
@@ -6142,6 +6229,7 @@ public static Set findAll(Set self, BiPredicate super T, ? super
* @return a List of matching values
* @since 2.4.0
*/
+ @SupportsLoopControl
public static List findAll(List self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
return (List) findAll((Collection) self, closure);
}
@@ -6155,6 +6243,7 @@ public static List findAll(List self, @ClosureParams(FirstParam.FirstG
* @return a Collection of matching values
* @since 1.5.6
*/
+ @SupportsLoopControl
public static Collection findAll(Collection self, @ClosureParams(FirstParam.FirstGenericType.class) Closure closure) {
return findMany(createSimilarCollection(self), self.iterator(), closure);
}
@@ -6282,6 +6371,7 @@ public static Collection findAll(Collection self) {
* @since 1.6.0
*/
@SuppressWarnings("unchecked")
+ @SupportsLoopControl
public static List findAll(Object self, Closure closure) {
return findMany(new ArrayList(), InvokerHelper.asIterator(self), closure);
}
@@ -6308,7 +6398,14 @@ static > C findMany(C collector, Iterator extends T
BooleanClosureWrapper test = new BooleanClosureWrapper(closure);
while (iter.hasNext()) {
T value = iter.next();
- if (test.call(value)) {
+ boolean keep;
+ try {
+ keep = test.call(value);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ continue;
+ }
+ if (keep) {
collector.add(value);
}
}
@@ -9539,6 +9636,7 @@ public static List> inits(Iterable self) {
* @see #inject(Object, Object, Closure)
* @since 1.8.7
*/
+ @SupportsLoopControl
public static T inject(Object self, @ClosureParams(value=FromString.class,options="T,?") Closure closure) {
Iterator> iter = InvokerHelper.asIterator(self);
if (!iter.hasNext()) {
@@ -9573,6 +9671,7 @@ public static T inject(Object self, @ClosureParams(value=FromSt
* @see #inject(Iterable, Object, Closure)
* @since 5.0.0
*/
+ @SupportsLoopControl
public static T inject(Iterable self, @ClosureParams(value=FromString.class,options="T,E") Closure closure) {
Iterator iter = self.iterator();
if (!iter.hasNext()) {
@@ -9629,6 +9728,7 @@ public static T inject(Iterable self, BinaryOperator operator) {
* @see #inject(Iterator, Object, Closure)
* @since 1.5.0
*/
+ @SupportsLoopControl
public static T inject(Object self, U initialValue, @ClosureParams(value=FromString.class,options="T,?") Closure closure) {
Iterator iter = InvokerHelper.asIterator(self);
return (T) inject(iter, initialValue, closure);
@@ -9676,6 +9776,7 @@ public static T inject(Object self, U initialValue
* @return the result of the last closure call
* @since 5.0.0
*/
+ @SupportsLoopControl
public static T inject(Iterable self, U initialValue, @ClosureParams(value=FromString.class,options="T,E") Closure closure) {
return inject(self.iterator(), initialValue, closure);
}
@@ -9725,13 +9826,19 @@ public static U inject(Iterable self, U initialValue, BiFunction sup
* @return the result of the last closure call
* @since 1.5.0
*/
+ @SupportsLoopControl
public static T inject(Iterator self, U initialValue, @ClosureParams(value=FromString.class,options="T,E") Closure closure) {
T value = initialValue;
Object[] params = new Object[2];
while (self.hasNext()) {
params[0] = value;
params[1] = self.next();
- value = closure.call(params);
+ try {
+ value = closure.call(params);
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ // CONTINUE: keep the prior accumulator value
+ }
}
return value;
}
@@ -9760,13 +9867,19 @@ public static T inject(Iterator self, U init
* @return the result of the last closure call
* @since 1.8.1
*/
+ @SupportsLoopControl
public static T inject(Map self, U initialValue, @ClosureParams(value=FromString.class,options={"T,Map.Entry","T,K,V"}) Closure closure) {
T value = initialValue;
for (Map.Entry entry : self.entrySet()) {
- if (closure.getMaximumNumberOfParameters() == 3) {
- value = closure.call(value, entry.getKey(), entry.getValue());
- } else {
- value = closure.call(value, entry);
+ try {
+ if (closure.getMaximumNumberOfParameters() == 3) {
+ value = closure.call(value, entry.getKey(), entry.getValue());
+ } else {
+ value = closure.call(value, entry);
+ }
+ } catch (LoopControl signal) {
+ if (signal == LoopControl.BREAK) break;
+ // CONTINUE: keep the prior accumulator value
}
}
return value;
@@ -15422,6 +15535,7 @@ public static String sprintf(Object self, String format, Object arg) {
* @param closure the closure to call
* @since 1.0
*/
+ @SupportsLoopControl
public static void step(Number self, Number to, Number stepNumber, Closure closure) {
if (self instanceof BigDecimal || to instanceof BigDecimal || stepNumber instanceof BigDecimal) {
final BigDecimal zero = BigDecimal.valueOf(0, 1); // Same as "0.0".
@@ -15430,11 +15544,11 @@ public static void step(Number self, Number to, Number stepNumber, Closure closu
BigDecimal stepNumber1 = NumberMath.toBigDecimal(stepNumber);
if (stepNumber1.compareTo(zero) > 0 && to1.compareTo(self1) > 0) {
for (BigDecimal i = self1; i.compareTo(to1) < 0; i = i.add(stepNumber1)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else if (stepNumber1.compareTo(zero) < 0 && to1.compareTo(self1) < 0) {
for (BigDecimal i = self1; i.compareTo(to1) > 0; i = i.add(stepNumber1)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else if(self1.compareTo(to1) != 0)
throw new GroovyRuntimeException("Infinite loop in " + self1 + ".step(" + to1 + ", " + stepNumber1 + ")");
@@ -15445,11 +15559,11 @@ public static void step(Number self, Number to, Number stepNumber, Closure closu
BigInteger stepNumber1 = (stepNumber instanceof BigInteger) ? (BigInteger) stepNumber : new BigInteger(stepNumber.toString());
if (stepNumber1.compareTo(zero) > 0 && to1.compareTo(self1) > 0) {
for (BigInteger i = self1; i.compareTo(to1) < 0; i = i.add(stepNumber1)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else if (stepNumber1.compareTo(zero) < 0 && to1.compareTo(self1) < 0) {
for (BigInteger i = self1; i.compareTo(to1) > 0; i = i.add(stepNumber1)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else if(self1.compareTo(to1) != 0)
throw new GroovyRuntimeException("Infinite loop in " + self1 + ".step(" + to1 + ", " + stepNumber1 + ")");
@@ -15459,11 +15573,11 @@ public static void step(Number self, Number to, Number stepNumber, Closure closu
int stepNumber1 = stepNumber.intValue();
if (stepNumber1 > 0 && to1 > self1) {
for (int i = self1; i < to1; i += stepNumber1) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else if (stepNumber1 < 0 && to1 < self1) {
for (int i = self1; i > to1; i += stepNumber1) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else if(self1 != to1)
throw new GroovyRuntimeException("Infinite loop in " + self1 + ".step(" + to1 + ", " + stepNumber1 + ")");
@@ -16452,9 +16566,10 @@ public void remove() {
* @param closure the closure to call a number of times
* @since 1.0
*/
+ @SupportsLoopControl
public static void times(Number self, @ClosureParams(value=SimpleType.class,options="int") Closure closure) {
for (int i = 0, size = self.intValue(); i < size; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
if (closure.getDirective() == Closure.DONE) {
break;
}
@@ -18537,12 +18652,13 @@ public Iterator iterator() {
* @param closure the closure to call
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(Number self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
int self1 = self.intValue();
int to1 = to.intValue();
if (self1 <= to1) {
for (int i = self1; i <= to1; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18558,11 +18674,12 @@ public static void upto(Number self, Number to, @ClosureParams(FirstParam.class)
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(long self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
long to1 = to.longValue();
if (self <= to1) {
for (long i = self; i <= to1; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18578,11 +18695,12 @@ public static void upto(long self, Number to, @ClosureParams(FirstParam.class) C
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(Long self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
long to1 = to.longValue();
if (self <= to1) {
for (long i = self; i <= to1; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18598,11 +18716,12 @@ public static void upto(Long self, Number to, @ClosureParams(FirstParam.class) C
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(float self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
float to1 = to.floatValue();
if (self <= to1) {
for (float i = self; i <= to1; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18618,11 +18737,12 @@ public static void upto(float self, Number to, @ClosureParams(FirstParam.class)
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(Float self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
float to1 = to.floatValue();
if (self <= to1) {
for (float i = self; i <= to1; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18638,11 +18758,12 @@ public static void upto(Float self, Number to, @ClosureParams(FirstParam.class)
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(double self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
double to1 = to.doubleValue();
if (self <= to1) {
for (double i = self; i <= to1; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18658,11 +18779,12 @@ public static void upto(double self, Number to, @ClosureParams(FirstParam.class)
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(Double self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
double to1 = to.doubleValue();
if (self <= to1) {
for (double i = self; i <= to1; i++) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18682,13 +18804,14 @@ public static void upto(Double self, Number to, @ClosureParams(FirstParam.class)
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(BigInteger self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
if (to instanceof BigDecimal to1) {
final BigDecimal one = BigDecimal.valueOf(10, 1);
BigDecimal self1 = new BigDecimal(self);
if (self1.compareTo(to1) <= 0) {
for (BigDecimal i = self1; i.compareTo(to1) <= 0; i = i.add(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException(
@@ -18699,7 +18822,7 @@ public static void upto(BigInteger self, Number to, @ClosureParams(FirstParam.cl
final BigInteger one = BigInteger.valueOf(1);
if (self.compareTo(to1) <= 0) {
for (BigInteger i = self; i.compareTo(to1) <= 0; i = i.add(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException(
@@ -18710,7 +18833,7 @@ public static void upto(BigInteger self, Number to, @ClosureParams(FirstParam.cl
BigInteger to1 = new BigInteger(to.toString());
if (self.compareTo(to1) <= 0) {
for (BigInteger i = self; i.compareTo(to1) <= 0; i = i.add(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException(MessageFormat.format(
@@ -18732,12 +18855,13 @@ public static void upto(BigInteger self, Number to, @ClosureParams(FirstParam.cl
* @param closure the code to execute for each number
* @since 1.0
*/
+ @SupportsLoopControl
public static void upto(BigDecimal self, Number to, @ClosureParams(FirstParam.class) Closure closure) {
final BigDecimal one = BigDecimal.valueOf(10, 1); // That's what you get for "1.0".
if (to instanceof BigDecimal to1) {
if (self.compareTo(to1) <= 0) {
for (BigDecimal i = self; i.compareTo(to1) <= 0; i = i.add(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18746,7 +18870,7 @@ public static void upto(BigDecimal self, Number to, @ClosureParams(FirstParam.cl
BigDecimal to1 = new BigDecimal((BigInteger) to);
if (self.compareTo(to1) <= 0) {
for (BigDecimal i = self; i.compareTo(to1) <= 0; i = i.add(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
@@ -18755,7 +18879,7 @@ public static void upto(BigDecimal self, Number to, @ClosureParams(FirstParam.cl
BigDecimal to1 = NumberMath.toBigDecimal(to);
if (self.compareTo(to1) <= 0) {
for (BigDecimal i = self; i.compareTo(to1) <= 0; i = i.add(one)) {
- closure.call(i);
+ if (!callResumable(closure, i)) break;
}
} else
throw new GroovyRuntimeException("The argument (" + to +
diff --git a/src/main/java/org/codehaus/groovy/runtime/LoopControl.java b/src/main/java/org/codehaus/groovy/runtime/LoopControl.java
new file mode 100644
index 00000000000..56768c6e3f6
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/runtime/LoopControl.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.runtime;
+
+/**
+ * Compiler-internal loop-control signals used to implement {@code break} and
+ * {@code continue} inside closures passed to cooperating iterator methods
+ * (GROOVY-12126). The compiler lowers {@code break}/{@code continue} in an
+ * eligible closure to {@code throw LoopControl.BREAK}/{@code CONTINUE};
+ * iterator methods marked {@link groovy.transform.SupportsLoopControl} catch
+ * the signal per element and stop or skip accordingly.
+ *
+ * The two signals are preallocated, carry no stack trace, and cannot be
+ * instantiated by user code, so the per-element cost of a cooperating loop is
+ * a try/catch region that is free unless a signal is actually thrown. If a
+ * signal reaches a non-cooperating caller it surfaces with an explanatory
+ * message rather than being silently swallowed.
+ *
+ * @since 6.0.0
+ */
+public final class LoopControl extends RuntimeException {
+
+ private static final long serialVersionUID = 7972103766131505698L;
+
+ /** Signal that the current iteration should stop (loop {@code break}). */
+ public static final LoopControl BREAK = new LoopControl("break");
+
+ /** Signal that the current element should be skipped (loop {@code continue}). */
+ public static final LoopControl CONTINUE = new LoopControl("continue");
+
+ private LoopControl(final String kind) {
+ super("'" + kind + "' used in a closure passed to a method that does not support loop control",
+ null, false, false);
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/runtime/NonLocalReturn.java b/src/main/java/org/codehaus/groovy/runtime/NonLocalReturn.java
new file mode 100644
index 00000000000..c1ee9c8c132
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/runtime/NonLocalReturn.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.runtime;
+
+/**
+ * Compiler-internal signal used to implement non-local return from closures
+ * ({@code return@methodName expr}). The compiler synthesizes a unique token per
+ * activation of the target method and wraps the method body in a handler that
+ * catches this exception, returning the carried value when the token matches.
+ *
+ * Instances carry no stack trace, so the throw/catch round trip is cheap. If a
+ * closure containing a non-local return escapes and is invoked after its target
+ * activation has completed, the token no longer matches any live handler and
+ * this exception surfaces to the caller rather than corrupting an unrelated
+ * activation.
+ *
+ * This class is not intended for direct use in user code.
+ *
+ * @since 6.0.0
+ */
+public class NonLocalReturn extends RuntimeException {
+
+ private static final long serialVersionUID = -6045879712662530680L;
+
+ private final transient Object token;
+ private final transient Object value;
+
+ public NonLocalReturn(final Object token, final Object value) {
+ super("Non-local return escaped: the closure was invoked outside the dynamic extent of its target method activation",
+ null, false, false);
+ this.token = token;
+ this.value = value;
+ }
+
+ /**
+ * The per-activation token identifying the target method invocation.
+ */
+ public Object getToken() {
+ return token;
+ }
+
+ /**
+ * The value to be returned from the target method.
+ */
+ public Object getValue() {
+ return value;
+ }
+
+ /**
+ * Whether this signal targets the activation identified by the given token.
+ */
+ public boolean matches(final Object candidate) {
+ return token == candidate;
+ }
+
+ /**
+ * Throws a {@code NonLocalReturn} carrying the given token and value.
+ * Called from compiler-generated code at {@code return@target} sites;
+ * declared with a return type so call sites can occupy value positions.
+ */
+ public static Object raise(final Object token, final Object value) {
+ throw new NonLocalReturn(token, value);
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
index 0e7b4d99ce5..6ba63777eb7 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -390,6 +390,8 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
protected static final ClassNode CLOSUREPARAMS_CLASSNODE = ClassHelper.make(ClosureParams.class);
/** Cached {@link NamedParams} annotation type. */
protected static final ClassNode NAMED_PARAMS_CLASSNODE = ClassHelper.make(NamedParams.class);
+ /** Cached {@link groovy.transform.SupportsLoopControl} annotation type. */
+ protected static final ClassNode SUPPORTS_LOOP_CONTROL = ClassHelper.make(groovy.transform.SupportsLoopControl.class);
/** Cached {@link NamedParam} annotation type. */
protected static final ClassNode NAMED_PARAM_CLASSNODE = ClassHelper.make(NamedParam.class);
@Deprecated(forRemoval = true, since = "4.0.0")
@@ -3680,6 +3682,7 @@ protected void visitMethodCallArguments(final ClassNode receiver, final Argument
expression.putNodeMetaData(PARAMETER_TYPE, targetType);
}
if (expression instanceof ClosureExpression) {
+ checkLoopControlSupported(selectedMethod, expression);
checkClosureWithDelegatesTo(receiver, selectedMethod, args(expressions), parameters, expression, target);
if (i > 0 || !(selectedMethod instanceof ExtensionMethodNode)) {
inferClosureParameterTypes(receiver, arguments, (ClosureExpression) expression, target, selectedMethod);
@@ -4094,6 +4097,21 @@ private void resolveGenericsFromTypeHint(final ClassNode receiver, final Express
}
}
+ /**
+ * Checks that a closure using break/continue (tagged by the non-local
+ * control flow rewriter, GROOVY-12126) is passed to a method that
+ * cooperates with the loop-control protocol.
+ */
+ private void checkLoopControlSupported(final MethodNode mn, final Expression closure) {
+ if (closure.getNodeMetaData(org.codehaus.groovy.control.NonLocalControlFlowRewriter.class) == null) return;
+ MethodNode target = mn instanceof ExtensionMethodNode ? ((ExtensionMethodNode) mn).getExtensionMethodNode() : mn;
+ if (target.getAnnotations(SUPPORTS_LOOP_CONTROL).isEmpty()) {
+ addStaticTypeError("closure uses break/continue but the target method " +
+ prettyPrintTypeName(target.getDeclaringClass()) + "#" + target.getName() +
+ " is not marked @SupportsLoopControl", closure);
+ }
+ }
+
private void checkClosureWithDelegatesTo(final ClassNode receiver, final MethodNode mn, final ArgumentListExpression arguments, final Parameter[] params, final Expression expression, final Parameter param) {
List annotations = param.getAnnotations(DELEGATES_TO);
if (annotations != null && !annotations.isEmpty()) {
diff --git a/src/test-resources/core/ReturnAt_01.groovy b/src/test-resources/core/ReturnAt_01.groovy
new file mode 100644
index 00000000000..fbe897fd374
--- /dev/null
+++ b/src/test-resources/core/ReturnAt_01.groovy
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+def firstMatch(rows, pred) {
+ rows.each { row ->
+ row.each { cell ->
+ if (pred(cell)) return@firstMatch cell
+ }
+ }
+ null
+}
+
+def m() {
+ [1, 2, 3].each {
+ if (it > 2) return@m
+ }
+ [1, 2, 3].each {
+ return @m it * 2
+ }
+ return@m 42;
+}
+
+void n() {
+ def c = { return@n }
+}
+
+[[1]].each { return@script it }
diff --git a/src/test-resources/fail/ReturnAt_01x.groovy b/src/test-resources/fail/ReturnAt_01x.groovy
new file mode 100644
index 00000000000..2a9e5a47248
--- /dev/null
+++ b/src/test-resources/fail/ReturnAt_01x.groovy
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+def m() {
+ [1].each {
+ return@1 it
+ }
+}
diff --git a/src/test-resources/fail/ReturnAt_02x.groovy b/src/test-resources/fail/ReturnAt_02x.groovy
new file mode 100644
index 00000000000..3f3c668eb1d
--- /dev/null
+++ b/src/test-resources/fail/ReturnAt_02x.groovy
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+def m() {
+ [1].each {
+ return@ 99
+ }
+}
diff --git a/src/test/groovy/gls/closures/LoopControlTest.groovy b/src/test/groovy/gls/closures/LoopControlTest.groovy
new file mode 100644
index 00000000000..2350d0e9b08
--- /dev/null
+++ b/src/test/groovy/gls/closures/LoopControlTest.groovy
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package gls.closures
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Tests for break/continue inside closure arguments to cooperating iterator
+ * methods (GROOVY-12126).
+ */
+final class LoopControlTest {
+
+ private static final GroovyShell CS_SHELL = new GroovyShell(
+ new CompilerConfiguration().addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic)))
+
+ /** Runs the script dynamically and under @CompileStatic. */
+ private static void assertScriptBoth(String text) {
+ assertScript(text)
+ assertScript(CS_SHELL, text)
+ }
+
+ @Test
+ void testEachBreakContinue() {
+ assertScriptBoth '''
+ def seen = []
+ [1, 2, 3, 4, 5].each {
+ if (it == 2) continue
+ if (it == 4) break
+ seen << it
+ }
+ assert seen == [1, 3]
+ '''
+ }
+
+ @Test
+ void testTimesUptoStep() {
+ assertScriptBoth '''
+ def seen = []
+ 5.times {
+ if (it == 3) break
+ seen << it
+ }
+ assert seen == [0, 1, 2]
+
+ seen = []
+ 1.upto(9) {
+ if (it % 2 == 0) continue
+ if (it > 5) break
+ seen << it
+ }
+ assert seen == [1, 3, 5]
+
+ seen = []
+ 0.step(10, 2) {
+ if (it == 4) continue
+ if (it == 8) break
+ seen << it
+ }
+ assert seen == [0, 2, 6]
+ '''
+ }
+
+ @Test
+ void testCollectExcludesAndSkips() {
+ assertScriptBoth '''
+ assert [1, 2, 3, 4].collect {
+ if (it == 2) continue
+ if (it == 4) break
+ it * 10
+ } == [10, 30]
+ '''
+ }
+
+ @Test
+ void testFindAllAndInject() {
+ assertScriptBoth '''
+ assert [1, 2, 3, 4, 5].findAll {
+ if (it == 4) break
+ it % 2 == 1
+ } == [1, 3]
+
+ assert [1, 2, 3, 4].inject(0) { acc, it ->
+ if (it == 2) continue
+ if (it == 4) break
+ acc + it
+ } == 4
+ '''
+ }
+
+ @Test
+ void testMapIteration() {
+ assertScriptBoth '''
+ def seen = []
+ [a: 1, b: 2, c: 3].each { k, v ->
+ if (v == 2) continue
+ if (v == 3) break
+ seen << k
+ }
+ assert seen == ['a']
+ '''
+ }
+
+ @Test
+ void testNestedClosuresBindInnermostIteration() {
+ assertScriptBoth '''
+ def seen = []
+ [[1, 2, 3], [4, 5]].each { row ->
+ row.each { cell ->
+ if (cell == 2 || cell == 5) break
+ seen << cell
+ }
+ }
+ assert seen == [1, 4]
+ '''
+ }
+
+ @Test
+ void testBreakBoundToRealLoopInsideClosureIsUntouched() {
+ assertScriptBoth '''
+ def seen = []
+ [1, 2].each { outer ->
+ for (i in 10..14) {
+ if (i == 12) break
+ seen << i
+ }
+ seen << outer
+ }
+ assert seen == [10, 11, 1, 10, 11, 2]
+ '''
+ }
+
+ @Test
+ void testContinueBoundToRealLoopInsideClosureIsUntouched() {
+ assertScriptBoth '''
+ def seen = []
+ [1].each {
+ for (i in 1..5) {
+ if (i % 2 == 0) continue
+ seen << i
+ }
+ }
+ assert seen == [1, 3, 5]
+ '''
+ }
+
+ @Test
+ void testBreakBoundToSwitchInsideClosureIsUntouched() {
+ assertScriptBoth '''
+ def seen = []
+ [1, 2, 3].each {
+ switch (it) {
+ case 2:
+ seen << 'two'
+ break
+ default:
+ seen << it
+ }
+ }
+ assert seen == [1, 'two', 3]
+ '''
+ }
+
+ @Test
+ void testUncooperativeCalleeEscapesLoudlyWhenDynamic() {
+ assertScript '''
+ import org.codehaus.groovy.runtime.LoopControl
+
+ try {
+ 'x'.with { break }
+ assert false, 'expected LoopControl to escape'
+ } catch (LoopControl expected) {
+ assert expected.message.contains('does not support loop control')
+ }
+ '''
+ }
+
+ @Test
+ void testUncooperativeCalleeIsCompileErrorUnderStaticTypeChecking() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ @groovy.transform.TypeChecked
+ void m() {
+ 'x'.with { break }
+ }
+ m()
+ '''
+ assert err.message.contains('is not marked @SupportsLoopControl')
+ }
+
+ @Test
+ void testBreakOutsideCallArgumentClosureIsCompileError() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ def c = { break }
+ '''
+ assert err.message.contains('break inside a closure is only allowed when the closure is a direct argument of a method call')
+ }
+
+ @Test
+ void testContinueOutsideCallArgumentClosureIsCompileError() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ def c = { continue }
+ '''
+ assert err.message.contains('continue inside a closure is only allowed when the closure is a direct argument of a method call')
+ }
+
+ @Test
+ void testLabeledBreakInClosureIsCompileError() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ outer:
+ for (i in 1..3) {
+ [1].each { break outer }
+ }
+ '''
+ assert err.message.contains('labeled break is not allowed inside a closure')
+ }
+
+ @Test
+ void testBreakInClosureInsideLexicalLoopBindsIteration() {
+ // previously leaked through LabelVerifier and broke at codegen; now well-defined
+ assertScriptBoth '''
+ def seen = []
+ for (i in 1..2) {
+ [10, 20, 30].each {
+ if (it == 20) break
+ seen << it
+ }
+ }
+ assert seen == [10, 10]
+ '''
+ }
+}
diff --git a/src/test/groovy/gls/closures/NonLocalReturnTest.groovy b/src/test/groovy/gls/closures/NonLocalReturnTest.groovy
new file mode 100644
index 00000000000..58d4185ec31
--- /dev/null
+++ b/src/test/groovy/gls/closures/NonLocalReturnTest.groovy
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package gls.closures
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Tests for non-local return from closures via {@code return@target} (GROOVY-12126).
+ */
+final class NonLocalReturnTest {
+
+ private static final GroovyShell CS_SHELL = new GroovyShell(
+ new CompilerConfiguration().addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic)))
+
+ /** Runs the script dynamically and under @CompileStatic. */
+ private static void assertScriptBoth(String text) {
+ assertScript(text)
+ assertScript(CS_SHELL, text)
+ }
+
+ @Test
+ void testReturnFromNestedClosures() {
+ assertScriptBoth '''
+ def firstMatch(List> rows, Closure pred) {
+ rows.each { row ->
+ row.each { cell ->
+ if (pred(cell)) return@firstMatch cell
+ }
+ }
+ null
+ }
+ assert firstMatch([[1, 2], [3, 4]]) { Integer it -> it > 2 } == 3
+ assert firstMatch([[1, 2], [3, 4]]) { Integer it -> it > 9 } == null
+ '''
+ }
+
+ @Test
+ void testReturnWithoutValue() {
+ assertScriptBoth '''
+ def m(List items) {
+ def seen = []
+ items.each {
+ if (it > 2) return@m seen
+ seen << it
+ }
+ seen << 'end'
+ seen
+ }
+ assert m([1, 2, 3, 4]) == [1, 2]
+ assert m([1, 2]) == [1, 2, 'end']
+ '''
+ }
+
+ @Test
+ void testVoidMethod() {
+ assertScriptBoth '''
+ class C {
+ List seen = []
+ void scan(List items) {
+ items.each {
+ if (it == 0) return@scan
+ seen << it
+ }
+ seen << 'end'
+ }
+ }
+ def c = new C()
+ c.scan([1, 0, 2])
+ assert c.seen == [1]
+ '''
+ }
+
+ @Test
+ void testPrimitiveReturnType() {
+ assertScriptBoth '''
+ int firstBig(List items) {
+ items.each { if (it > 10) return@firstBig it }
+ -1
+ }
+ assert firstBig([1, 20, 3]) == 20
+ assert firstBig([1, 2, 3]) == -1
+ '''
+ }
+
+ @Test
+ void testStaticMethod() {
+ assertScriptBoth '''
+ class Util {
+ static firstEven(List nums) {
+ nums.each { if (it % 2 == 0) return@firstEven it }
+ null
+ }
+ }
+ assert Util.firstEven([1, 3, 4, 5]) == 4
+ '''
+ }
+
+ @Test
+ void testTargetAtMethodLevelIsPlainReturn() {
+ assertScriptBoth '''
+ def m() {
+ return@m 42
+ }
+ assert m() == 42
+ '''
+ }
+
+ @Test
+ void testRecursionUsesPerActivationToken() {
+ assertScriptBoth '''
+ def search(int depth) {
+ [1].each {
+ if (depth == 0) return@search 'hit'
+ }
+ 'wrapped(' + search(depth - 1) + ')'
+ }
+ assert search(2) == 'wrapped(wrapped(hit))'
+ '''
+ }
+
+ @Test
+ void testFinallyRunsDuringUnwind() {
+ assertScriptBoth '''
+ def m(List log, List items) {
+ try {
+ items.each {
+ log << it
+ if (it == 2) return@m 'done'
+ }
+ } finally {
+ log << 'fin'
+ }
+ 'end'
+ }
+ def log = []
+ assert m(log, [1, 2, 3]) == 'done'
+ assert log == [1, 2, 'fin']
+ '''
+ }
+
+ @Test
+ void testReturnFromScriptBody() {
+ assertScriptBoth '''
+ def seen = []
+ [1, 2, 3].each {
+ seen << it
+ if (it == 2) {
+ assert seen == [1, 2]
+ return@script
+ }
+ }
+ throw new IllegalStateException('should not be reached')
+ '''
+ }
+
+ @Test
+ void testEscapedClosureSurfacesLoudly() {
+ assertScriptBoth '''
+ import org.codehaus.groovy.runtime.NonLocalReturn
+
+ def maker(List stash) {
+ stash << { -> return@maker 1 }
+ 'made'
+ }
+ List stash = []
+ assert maker(stash) == 'made'
+ try {
+ stash[0].call()
+ assert false, 'expected NonLocalReturn to escape'
+ } catch (NonLocalReturn expected) {
+ }
+ '''
+ }
+
+ @Test
+ void testStaleActivationRethrowsNotHijacks() {
+ assertScriptBoth '''
+ import org.codehaus.groovy.runtime.NonLocalReturn
+
+ def maker(List stash, boolean capture) {
+ if (capture) {
+ stash << { -> return@maker 'stolen' }
+ return 'made'
+ }
+ stash[0].call()
+ 'not reached'
+ }
+ List stash = []
+ assert maker(stash, true) == 'made'
+ try {
+ maker(stash, false)
+ assert false, 'expected NonLocalReturn to escape the mismatched activation'
+ } catch (NonLocalReturn expected) {
+ }
+ '''
+ }
+
+ @Test
+ void testUnknownTargetIsCompileError() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ def m() {
+ [1].each { return@nosuch 1 }
+ }
+ '''
+ assert err.message.contains("no lexically enclosing method named 'nosuch'")
+ }
+
+ @Test
+ void testCrossClassBoundaryIsCompileError() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ class C {
+ def outer(items) {
+ def r = new Runnable() {
+ void run() {
+ [1].each { return@outer 1 }
+ }
+ }
+ r.run()
+ }
+ }
+ '''
+ assert err.message.contains('cannot return across a class boundary')
+ }
+
+ @Test
+ void testConstructorIsCompileError() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ class C {
+ C() {
+ [1].each { return@C 1 }
+ }
+ }
+ '''
+ assert err.message.contains('only allowed inside a method body')
+ }
+
+ @Test
+ void testScriptTargetOutsideScriptBodyIsCompileError() {
+ def err = shouldFail MultipleCompilationErrorsException, '''
+ class C {
+ def m() {
+ [1].each { return@script 1 }
+ }
+ }
+ '''
+ assert err.message.contains("no lexically enclosing method named 'script'")
+ }
+}
diff --git a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
index 3ca7c7b28cc..b45bd5b1d10 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -342,6 +342,11 @@ final class GroovyParserTest {
doTest('core/Return_01.groovy')
}
+ @Test
+ void 'groovy core - ReturnAt'() {
+ doTest('core/ReturnAt_01.groovy')
+ }
+
@Test
void 'groovy core - Throw'() {
doTest('core/Throw_01.groovy')
diff --git a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index bbe0175f72c..fa2fc54f7b6 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -78,6 +78,12 @@ final class SyntaxErrorTest {
TestUtils.doRunAndShouldFail('fail/Break_02x.groovy')
}
+ @Test
+ void 'groovy core - ReturnAt'() {
+ TestUtils.doRunAndShouldFail('fail/ReturnAt_01x.groovy')
+ TestUtils.doRunAndShouldFail('fail/ReturnAt_02x.groovy')
+ }
+
@Test
void 'groovy core - UnexpectedCharacter 1'() {
TestUtils.doRunAndShouldFail('fail/UnexpectedCharacter_01x.groovy')
diff --git a/src/test/groovy/org/codehaus/groovy/runtime/LoopControlDGMTest.groovy b/src/test/groovy/org/codehaus/groovy/runtime/LoopControlDGMTest.groovy
new file mode 100644
index 00000000000..3ec3cf92f59
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/runtime/LoopControlDGMTest.groovy
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.runtime
+
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertThrows
+
+/**
+ * Tests the loop-control runtime protocol (GROOVY-12126): DGM iterator methods
+ * marked with {@code @SupportsLoopControl} catch the {@link LoopControl}
+ * signals around each per-element closure invocation. The signals are thrown
+ * directly here; the compiler sugar (break/continue in closures) has its own
+ * tests in gls.closures.LoopControlTest.
+ */
+final class LoopControlDGMTest {
+
+ @Test
+ void testEachBreak() {
+ def seen = []
+ [1, 2, 3, 4].each {
+ if (it == 3) throw LoopControl.BREAK
+ seen << it
+ }
+ assert seen == [1, 2]
+ }
+
+ @Test
+ void testEachContinue() {
+ def seen = []
+ [1, 2, 3, 4].each {
+ if (it % 2 == 0) throw LoopControl.CONTINUE
+ seen << it
+ }
+ assert seen == [1, 3]
+ }
+
+ @Test
+ void testEachIterator() {
+ def seen = []
+ [1, 2, 3].iterator().each {
+ if (it == 2) throw LoopControl.BREAK
+ seen << it
+ }
+ assert seen == [1]
+ }
+
+ @Test
+ void testEachMap() {
+ def seen = []
+ [a: 1, b: 2, c: 3].each { k, v ->
+ if (v == 2) throw LoopControl.CONTINUE
+ if (v == 3) throw LoopControl.BREAK
+ seen << k
+ }
+ assert seen == ['a']
+ }
+
+ @Test
+ void testEachWithIndex() {
+ def seen = []
+ ['a', 'b', 'c', 'd'].eachWithIndex { item, i ->
+ if (item == 'b') throw LoopControl.CONTINUE
+ if (i == 3) throw LoopControl.BREAK
+ seen << "$item$i".toString()
+ }
+ assert seen == ['a0', 'c2']
+ }
+
+ @Test
+ void testEachWithIndexMap() {
+ def seen = []
+ [a: 1, b: 2, c: 3].eachWithIndex { entry, i ->
+ if (i == 1) throw LoopControl.CONTINUE
+ if (i == 2) throw LoopControl.BREAK
+ seen << entry.key
+ }
+ assert seen == ['a']
+ }
+
+ @Test
+ void testTimes() {
+ def seen = []
+ 5.times {
+ if (it == 1) throw LoopControl.CONTINUE
+ if (it == 3) throw LoopControl.BREAK
+ seen << it
+ }
+ assert seen == [0, 2]
+ }
+
+ @Test
+ void testUptoDowntoStep() {
+ def seen = []
+ 1.upto(5) {
+ if (it == 4) throw LoopControl.BREAK
+ seen << it
+ }
+ assert seen == [1, 2, 3]
+
+ seen = []
+ 5.downto(1) {
+ if (it == 4) throw LoopControl.CONTINUE
+ if (it == 2) throw LoopControl.BREAK
+ seen << it
+ }
+ assert seen == [5, 3]
+
+ seen = []
+ 1.0.upto(3.0) {
+ if (it == 2.0) throw LoopControl.BREAK
+ seen << it
+ }
+ assert seen == [1.0]
+
+ seen = []
+ 0.step(10, 2) {
+ if (it == 6) throw LoopControl.BREAK
+ seen << it
+ }
+ assert seen == [0, 2, 4]
+ }
+
+ @Test
+ void testCollectBreakExcludesCurrentElement() {
+ assert [1, 2, 3, 4].collect {
+ if (it == 3) throw LoopControl.BREAK
+ it * 10
+ } == [10, 20]
+ }
+
+ @Test
+ void testCollectContinueSkipsContribution() {
+ assert [1, 2, 3, 4].collect {
+ if (it % 2 == 0) throw LoopControl.CONTINUE
+ it * 10
+ } == [10, 30]
+ }
+
+ @Test
+ void testCollectWithCollector() {
+ assert [1, 2, 3].collect(new LinkedList()) {
+ if (it == 3) throw LoopControl.BREAK
+ it + 1
+ } == [2, 3]
+ }
+
+ @Test
+ void testCollectMap() {
+ assert [a: 1, b: 2, c: 3].collect { k, v ->
+ if (v == 1) throw LoopControl.CONTINUE
+ if (v == 3) throw LoopControl.BREAK
+ "$k$v".toString()
+ } == ['b2']
+ }
+
+ @Test
+ void testFindAll() {
+ assert [1, 2, 3, 4, 5].findAll {
+ if (it == 2) throw LoopControl.CONTINUE
+ if (it == 4) throw LoopControl.BREAK
+ it % 2 == 1
+ } == [1, 3]
+ }
+
+ @Test
+ void testFindAllMap() {
+ assert [a: 1, b: 2, c: 3].findAll { k, v ->
+ if (v == 2) throw LoopControl.BREAK
+ true
+ } == [a: 1]
+ }
+
+ @Test
+ void testInjectBreakReturnsPriorAccumulator() {
+ assert [1, 2, 3, 4].inject(0) { acc, it ->
+ if (it == 3) throw LoopControl.BREAK
+ acc + it
+ } == 3
+ }
+
+ @Test
+ void testInjectContinueKeepsPriorValue() {
+ assert [1, 2, 3, 4].inject(0) { acc, it ->
+ if (it % 2 == 0) throw LoopControl.CONTINUE
+ acc + it
+ } == 4
+ }
+
+ @Test
+ void testInjectMap() {
+ assert [a: 1, b: 2, c: 3].inject(0) { acc, entry ->
+ if (entry.value == 2) throw LoopControl.CONTINUE
+ acc + entry.value
+ } == 4
+ }
+
+ @Test
+ void testClosureDoneStillHonored() {
+ def seen = []
+ def c
+ c = { seen << it; if (it == 1) c.directive = Closure.DONE }
+ 5.times(c)
+ assert seen == [0, 1]
+
+ def t
+ t = { if (it == 2) t.directive = Closure.DONE; it * 10 }
+ assert [1, 2, 3].collect([], t) == [10, 20]
+ }
+
+ @Test
+ void testSignalEscapesUncooperativeMethod() {
+ // 'with' is not a loop and does not cooperate; the signal must surface loudly
+ def e = assertThrows(LoopControl) {
+ 'x'.with { throw LoopControl.BREAK }
+ }
+ assert e.message.contains('does not support loop control')
+ }
+
+ @Test
+ void testSignalsCarryNoStackTrace() {
+ assert LoopControl.BREAK.stackTrace.length == 0
+ assert LoopControl.CONTINUE.stackTrace.length == 0
+ }
+}