This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 9d63f1625b GROOVY-11878: Invariant loops spike (#2400)
9d63f1625b is described below
commit 9d63f1625b1a352c8e791e435004ba455e2b56c3
Author: Paul King <[email protected]>
AuthorDate: Sat Mar 28 07:20:20 2026 +1100
GROOVY-11878: Invariant loops spike (#2400)
* GROOVY-11878: Allow AST transforms to be applicable in more places,
initially loop statements
* GROOVY-11878: Allow AST transforms to be applicable in more places,
initially loop statements (demo usage not in final form)
---
src/antlr/GroovyParser.g4 | 6 +-
.../groovy/transform/ASTTestTransformation.groovy | 2 +-
src/main/java/groovy/transform/Parallel.java | 41 ++++++
.../apache/groovy/parser/antlr4/AstBuilder.java | 12 +-
.../groovy/parser/antlr4/SemanticPredicates.java | 38 ++++-
.../org/codehaus/groovy/ast/AnnotationNode.java | 3 +
.../groovy/ast/ClassCodeExpressionTransformer.java | 3 +
.../groovy/ast/ClassCodeVisitorSupport.java | 13 ++
.../org/codehaus/groovy/ast/stmt/Statement.java | 40 ++++++
.../codehaus/groovy/control/ResolveVisitor.java | 11 ++
.../ASTTransformationCollectorCodeVisitor.java | 12 ++
.../groovy/transform/ASTTransformationVisitor.java | 15 ++
.../transform/ParallelASTTransformation.java | 109 ++++++++++++++
src/test-resources/core/AnnotatedLoop_01.groovy | 134 +++++++++++++++++
src/test-resources/core/AnnotatedLoop_02x.groovy | 117 +++++++++++++++
.../groovy/parser/antlr4/GroovyParserTest.groovy | 6 +
.../src/main/java/groovy/contracts/Invariant.java | 20 ++-
.../groovy/contracts/LoopInvariantViolation.java | 60 ++++++++
.../ast/LoopInvariantASTTransformation.java | 109 ++++++++++++++
.../groovy/contracts/domain/LoopInvariant.java | 44 ++++++
.../src/spec/doc/contracts-userguide.adoc | 47 +++++-
.../src/spec/test/ContractsTest.groovy | 46 ++++++
.../contracts/tests/inv/LoopInvariantTests.groovy | 159 +++++++++++++++++++++
23 files changed, 1033 insertions(+), 14 deletions(-)
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 88e3b8b713..67da533f72 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -600,9 +600,9 @@ switchStatement
;
loopStatement
- : FOR LPAREN forControl RPAREN nls statement
#forStmtAlt
- | WHILE expressionInPar nls statement
#whileStmtAlt
- | DO nls statement nls WHILE expressionInPar
#doWhileStmtAlt
+ : annotationsOpt FOR LPAREN forControl RPAREN nls statement
#forStmtAlt
+ | annotationsOpt WHILE expressionInPar nls statement
#whileStmtAlt
+ | annotationsOpt DO nls statement nls WHILE expressionInPar
#doWhileStmtAlt
;
continueStatement
diff --git
a/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy
b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy
index 897894bd17..e498a1b0d7 100644
--- a/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy
+++ b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy
@@ -81,7 +81,7 @@ class ASTTestTransformation implements ASTTransformation,
CompilationUnitAware {
annotationNode.setNodeMetaData(ASTTestTransformation, member)
annotationNode.setMember('value', new ClosureExpression(
Parameter.EMPTY_ARRAY, EmptyStatement.INSTANCE))
- member.variableScope.@parent = null
+ member.variableScope?.@parent = null
ISourceUnitOperation astTester = new ASTTester(astNode: nodes[1],
sourceUnit: source, testClosure:
annotationNode.getNodeMetaData(ASTTestTransformation))
for (int p = (phase ?: CompilePhase.SEMANTIC_ANALYSIS).phaseNumber, q
= (phase ?: CompilePhase.FINALIZATION).phaseNumber; p <= q; p += 1) {
diff --git a/src/main/java/groovy/transform/Parallel.java
b/src/main/java/groovy/transform/Parallel.java
new file mode 100644
index 0000000000..11c1fb0748
--- /dev/null
+++ b/src/main/java/groovy/transform/Parallel.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Runs each iteration of an annotated {@code for} loop on a separate thread.
+ * <p>
+ * This annotation is a lightweight demo transform and intentionally favors
simplicity
+ * over production-grade parallel orchestration.
+ *
+ * @since 6.0.0
+ * @see org.codehaus.groovy.transform.ParallelASTTransformation
+ */
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.ParallelASTTransformation")
+public @interface Parallel {
+}
+
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 ff07bc7fb1..07331823f8 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -470,7 +470,9 @@ public class AstBuilder extends
GroovyParserBaseVisitor<Object> {
Statement loopBody = this.unpackStatement((Statement)
this.visit(ctx.statement()));
- return configureAST(maker.apply(loopBody), ctx);
+ ForStatement forStatement = configureAST(maker.apply(loopBody), ctx);
+
visitAnnotationsOpt(ctx.annotationsOpt()).forEach(forStatement::addStatementAnnotation);
+ return forStatement;
}
@Override
@@ -571,14 +573,18 @@ public class AstBuilder extends
GroovyParserBaseVisitor<Object> {
public WhileStatement visitWhileStmtAlt(final WhileStmtAltContext ctx) {
Tuple2<BooleanExpression, Statement> conditionAndBlock =
createLoopConditionExpressionAndBlock(ctx.expressionInPar(), ctx.statement());
- return configureAST(new WhileStatement(conditionAndBlock.getV1(),
conditionAndBlock.getV2()), ctx);
+ WhileStatement whileStatement = configureAST(new
WhileStatement(conditionAndBlock.getV1(), conditionAndBlock.getV2()), ctx);
+
visitAnnotationsOpt(ctx.annotationsOpt()).forEach(whileStatement::addStatementAnnotation);
+ return whileStatement;
}
@Override
public DoWhileStatement visitDoWhileStmtAlt(final DoWhileStmtAltContext
ctx) {
Tuple2<BooleanExpression, Statement> conditionAndBlock =
createLoopConditionExpressionAndBlock(ctx.expressionInPar(), ctx.statement());
- return configureAST(new DoWhileStatement(conditionAndBlock.getV1(),
conditionAndBlock.getV2()), ctx);
+ DoWhileStatement doWhileStatement = configureAST(new
DoWhileStatement(conditionAndBlock.getV1(), conditionAndBlock.getV2()), ctx);
+
visitAnnotationsOpt(ctx.annotationsOpt()).forEach(doWhileStatement::addStatementAnnotation);
+ return doWhileStatement;
}
private Tuple2<BooleanExpression, Statement>
createLoopConditionExpressionAndBlock(final ExpressionInParContext eipc, final
StatementContext sc) {
diff --git
a/src/main/java/org/apache/groovy/parser/antlr4/SemanticPredicates.java
b/src/main/java/org/apache/groovy/parser/antlr4/SemanticPredicates.java
index addfd47d3c..e25d40f2b4 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/SemanticPredicates.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/SemanticPredicates.java
@@ -30,10 +30,13 @@ import java.util.List;
import java.util.regex.Pattern;
import static org.apache.groovy.parser.antlr4.GroovyParser.ASSIGN;
+import static org.apache.groovy.parser.antlr4.GroovyParser.AT;
import static
org.apache.groovy.parser.antlr4.GroovyParser.BuiltInPrimitiveType;
import static
org.apache.groovy.parser.antlr4.GroovyParser.CapitalizedIdentifier;
+import static org.apache.groovy.parser.antlr4.GroovyParser.DO;
import static org.apache.groovy.parser.antlr4.GroovyParser.DOT;
import static org.apache.groovy.parser.antlr4.GroovyParser.ExpressionContext;
+import static org.apache.groovy.parser.antlr4.GroovyParser.FOR;
import static org.apache.groovy.parser.antlr4.GroovyParser.Identifier;
import static org.apache.groovy.parser.antlr4.GroovyParser.LBRACK;
import static org.apache.groovy.parser.antlr4.GroovyParser.LPAREN;
@@ -41,7 +44,9 @@ import static org.apache.groovy.parser.antlr4.GroovyParser.LT;
import static
org.apache.groovy.parser.antlr4.GroovyParser.PathExpressionContext;
import static
org.apache.groovy.parser.antlr4.GroovyParser.PostfixExprAltContext;
import static
org.apache.groovy.parser.antlr4.GroovyParser.PostfixExpressionContext;
+import static org.apache.groovy.parser.antlr4.GroovyParser.RPAREN;
import static org.apache.groovy.parser.antlr4.GroovyParser.StringLiteral;
+import static org.apache.groovy.parser.antlr4.GroovyParser.WHILE;
import static org.apache.groovy.parser.antlr4.GroovyParser.YIELD;
import static org.apache.groovy.parser.antlr4.util.StringUtils.matches;
@@ -186,8 +191,39 @@ public class SemanticPredicates {
!(BuiltInPrimitiveType == tokenType ||
Arrays.binarySearch(MODIFIER_ARRAY, tokenType) >= 0)
&& !Character.isUpperCase(nextCodePoint)
&& nextCodePoint != '@'
- && !(ASSIGN == tokenType3 || (LT == tokenType2 ||
LBRACK == tokenType2));
+ && !(ASSIGN == tokenType3 || (LT == tokenType2 ||
LBRACK == tokenType2))
+ || (nextCodePoint == '@' && isAnnotatedLoopStatement(ts));
}
+ /**
+ * When the input starts with one or more annotations, scan past them and
check whether
+ * the first non-annotation token is a loop keyword ({@code for}, {@code
while}, {@code do}).
+ * If so, the construct is an annotated loop statement, NOT a local
variable declaration.
+ *
+ * @param ts the token stream positioned at the first annotation {@code @}
token
+ * @return {@code true} if annotations are followed by a loop keyword
+ */
+ static boolean isAnnotatedLoopStatement(TokenStream ts) {
+ int idx = 1; // ts.LT(1) is '@'
+ while (ts.LT(idx).getType() == AT) {
+ idx += 2; // skip AT and annotation name
+ // skip qualifier parts of a fully-qualified annotation name, e.g.
@java.lang.Deprecated
+ while (ts.LT(idx).getType() == DOT) {
+ idx += 2; // skip DOT and next name element
+ }
+ // skip annotation arguments (parenthesised), handling nesting
+ if (ts.LT(idx).getType() == LPAREN) {
+ idx++;
+ int depth = 1;
+ while (depth > 0 && ts.LT(idx).getType() !=
org.antlr.v4.runtime.Token.EOF) {
+ int t = ts.LT(idx++).getType();
+ if (t == LPAREN) depth++;
+ else if (t == RPAREN) depth--;
+ }
+ }
+ }
+ int afterAnnotations = ts.LT(idx).getType();
+ return afterAnnotations == FOR || afterAnnotations == WHILE ||
afterAnnotations == DO;
+ }
}
diff --git a/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
b/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
index 94b772f615..2ca4b0aab0 100644
--- a/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
@@ -50,6 +50,8 @@ public class AnnotationNode extends ASTNode {
public static final int TYPE_PARAMETER_TARGET = 1 << 8;
public static final int TYPE_USE_TARGET = 1 << 9;
public static final int RECORD_COMPONENT_TARGET = 1 << 10;
+ /** Groovy-only target for statement-level annotations (e.g. on {@code
for}/{@code while} loops). */
+ public static final int STATEMENT_TARGET = 1 << 11;
public static final int TYPE_TARGET = ANNOTATION_TARGET | 1;
// GROOVY-7151
private final ClassNode classNode;
@@ -257,6 +259,7 @@ public class AnnotationNode extends ASTNode {
case TYPE_PARAMETER_TARGET -> "TYPE_PARAMETER";
case TYPE_USE_TARGET -> "TYPE_USE";
case RECORD_COMPONENT_TARGET -> "RECORD_COMPONENT";
+ case STATEMENT_TARGET -> "STATEMENT";
default -> "unknown target";
};
}
diff --git
a/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java
b/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java
index 860f962084..71f9865667 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java
@@ -127,6 +127,7 @@ public abstract class ClassCodeExpressionTransformer
extends ClassCodeVisitorSup
@Override
public void visitDoWhileLoop(final DoWhileStatement stmt) {
+ visitStatementAnnotations(stmt);
stmt.getLoopBlock().visit(this);
stmt.setBooleanExpression((BooleanExpression)
transform(stmt.getBooleanExpression()));
}
@@ -138,6 +139,7 @@ public abstract class ClassCodeExpressionTransformer
extends ClassCodeVisitorSup
@Override
public void visitForLoop(final ForStatement stmt) {
+ visitStatementAnnotations(stmt);
if (!(stmt.getCollectionExpression() instanceof
ClosureListExpression)) {
visitAnnotations(stmt.getValueVariable()); // "for(T x : y)" or
"for(x in y)"
}
@@ -179,6 +181,7 @@ public abstract class ClassCodeExpressionTransformer
extends ClassCodeVisitorSup
@Override
public void visitWhileLoop(final WhileStatement stmt) {
+ visitStatementAnnotations(stmt);
stmt.setBooleanExpression((BooleanExpression)
transform(stmt.getBooleanExpression()));
stmt.getLoopBlock().visit(this);
}
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java
b/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java
index 166c0b3295..a03bd15f83 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java
@@ -200,6 +200,7 @@ public abstract class ClassCodeVisitorSupport extends
CodeVisitorSupport impleme
@Override
public void visitDoWhileLoop(DoWhileStatement statement) {
visitStatement(statement);
+ visitStatementAnnotations(statement);
super.visitDoWhileLoop(statement);
}
@@ -212,6 +213,7 @@ public abstract class ClassCodeVisitorSupport extends
CodeVisitorSupport impleme
@Override
public void visitForLoop(ForStatement statement) {
visitStatement(statement);
+ visitStatementAnnotations(statement);
if (statement.getValueVariable() != null) {
visitAnnotations(statement.getValueVariable());
}
@@ -257,6 +259,7 @@ public abstract class ClassCodeVisitorSupport extends
CodeVisitorSupport impleme
@Override
public void visitWhileLoop(WhileStatement statement) {
visitStatement(statement);
+ visitStatementAnnotations(statement);
super.visitWhileLoop(statement);
}
@@ -265,6 +268,16 @@ public abstract class ClassCodeVisitorSupport extends
CodeVisitorSupport impleme
protected void visitStatement(Statement statement) {
}
+ /**
+ * Called for each loop statement ({@code for}, {@code while}, {@code
do-while}) that
+ * carries statement-level annotations. Subclasses may override to process
those annotations.
+ *
+ * @param statement the loop statement that may have statement-level
annotations
+ * @since 6.0.0
+ */
+ protected void visitStatementAnnotations(Statement statement) {
+ }
+
protected abstract SourceUnit getSourceUnit();
@Override
diff --git a/src/main/java/org/codehaus/groovy/ast/stmt/Statement.java
b/src/main/java/org/codehaus/groovy/ast/stmt/Statement.java
index bc1d4b4c83..096143a08f 100644
--- a/src/main/java/org/codehaus/groovy/ast/stmt/Statement.java
+++ b/src/main/java/org/codehaus/groovy/ast/stmt/Statement.java
@@ -19,11 +19,14 @@
package org.codehaus.groovy.ast.stmt;
import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.Collections;
/**
* Base class for any statement.
@@ -58,6 +61,43 @@ public class Statement extends ASTNode {
});
}
+
//--------------------------------------------------------------------------
+ // Statement-level annotation support (Groovy-only; stored in node
metadata)
+
+ private static final Object STATEMENT_ANNOTATIONS_KEY =
"_statementAnnotations_";
+
+ /**
+ * Returns the list of statement-level annotations attached to this
statement.
+ * These are Groovy-only source-retention annotations that do not appear
at the
+ * JVM level; they are processed by registered {@link
org.codehaus.groovy.transform.ASTTransformation}s.
+ *
+ * @return an unmodifiable view of the annotations list, never {@code null}
+ * @since 6.0.0
+ */
+ @SuppressWarnings("unchecked")
+ public List<AnnotationNode> getStatementAnnotations() {
+ List<AnnotationNode> annotations =
getNodeMetaData(STATEMENT_ANNOTATIONS_KEY);
+ return annotations != null ? Collections.unmodifiableList(annotations)
: Collections.emptyList();
+ }
+
+ /**
+ * Attaches a statement-level annotation to this statement.
+ *
+ * @param annotation the annotation to attach
+ * @since 6.0.0
+ */
+ @SuppressWarnings("unchecked")
+ public void addStatementAnnotation(final AnnotationNode annotation) {
+ List<AnnotationNode> annotations =
getNodeMetaData(STATEMENT_ANNOTATIONS_KEY);
+ if (annotations == null) {
+ annotations = new ArrayList<>();
+ setNodeMetaData(STATEMENT_ANNOTATIONS_KEY, annotations);
+ }
+ annotations.add(Objects.requireNonNull(annotation));
+ }
+
+
//--------------------------------------------------------------------------
+
public boolean isEmpty() {
return false;
}
diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index 1251613f44..ec4335ceaf 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -1393,6 +1393,17 @@ public class ResolveVisitor extends
ClassCodeExpressionTransformer {
super.visitCatchStatement(cs);
}
+ /**
+ * Resolves the class nodes of any annotations attached to a loop statement
+ * (stored in statement metadata rather than in {@link
org.codehaus.groovy.ast.AnnotatedNode}).
+ */
+ @Override
+ protected void visitStatementAnnotations(final Statement statement) {
+ for (AnnotationNode annotation : statement.getStatementAnnotations()) {
+ visitAnnotation(annotation);
+ }
+ }
+
@Override
public void visitForLoop(final ForStatement forLoop) {
if (forLoop.getValueVariable() != null) {
diff --git
a/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java
b/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java
index 9df19d8e3b..47d0b90c98 100644
---
a/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java
+++
b/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java
@@ -26,6 +26,7 @@ import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.ExceptionMessage;
@@ -113,6 +114,17 @@ public class ASTTransformationCollectorCodeVisitor extends
ClassCodeVisitorSuppo
}
}
+ /**
+ * Collects AST transforms from statement-level annotations (e.g.
annotations placed
+ * directly on {@code for}/{@code while}/{@code do-while} loop statements).
+ */
+ @Override
+ protected void visitStatementAnnotations(final Statement statement) {
+ for (AnnotationNode annotation : statement.getStatementAnnotations()) {
+ addTransformsToClassNode(annotation);
+ }
+ }
+
private static void mergeCollectedAnnotations(final
AnnotationCollectorMode mode, final Map<Integer, List<AnnotationNode>>
existing, final List<AnnotationNode> replacements) {
switch (mode) {
case PREFER_COLLECTOR:
diff --git
a/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java
b/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java
index 7687a87a20..1c37dd1695 100644
--- a/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java
@@ -31,6 +31,7 @@ import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.GroovyClassVisitor;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.ASTTransformationsContext;
import org.codehaus.groovy.control.CompilationUnit;
@@ -173,6 +174,20 @@ public final class ASTTransformationVisitor extends
ClassCodeVisitorSupport {
}
}
+ /**
+ * Adds annotated loop statements to the target list so that the registered
+ * AST transformation is invoked with {@code nodes[1]} being the loop
statement.
+ */
+ @Override
+ protected void visitStatementAnnotations(final Statement statement) {
+ if (transforms == null) return;
+ for (AnnotationNode annotation : statement.getStatementAnnotations()) {
+ if (transforms.containsKey(annotation)) {
+ targetNodes.add(new ASTNode[]{annotation, statement});
+ }
+ }
+ }
+
private static final Tuple3<String, String, String>
COMPILEDYNAMIC_AND_COMPILESTATIC_AND_TYPECHECKED =
Tuple.tuple("groovy.transform.CompileDynamic",
"groovy.transform.CompileStatic", "groovy.transform.TypeChecked");
diff --git
a/src/main/java/org/codehaus/groovy/transform/ParallelASTTransformation.java
b/src/main/java/org/codehaus/groovy/transform/ParallelASTTransformation.java
new file mode 100644
index 0000000000..20e5a69f11
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/ParallelASTTransformation.java
@@ -0,0 +1,109 @@
+/*
+ * 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.transform;
+
+import groovy.transform.Parallel;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.VariableScope;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.ForStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+
+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.ctorX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+
+/**
+ * Demo AST transform for {@link Parallel}: each {@code for-in} iteration body
is
+ * wrapped in a new thread and started immediately.
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class ParallelASTTransformation implements ASTTransformation {
+
+ @Override
+ public void visit(final ASTNode[] nodes, final SourceUnit source) {
+ if (nodes == null || nodes.length != 2) return;
+ if (!(nodes[0] instanceof AnnotationNode annotation)) return;
+ if
(!Parallel.class.getName().equals(annotation.getClassNode().getName())) return;
+
+ if (!(nodes[1] instanceof ForStatement forStatement)) {
+ source.getErrorCollector().addError(new SyntaxErrorMessage(
+ new SyntaxException("@Parallel may only be applied to a
for loop statement",
+ annotation.getLineNumber(),
annotation.getColumnNumber()), source));
+ return;
+ }
+
+ if (forStatement.getValueVariable() == null) {
+ source.getErrorCollector().addError(new SyntaxErrorMessage(
+ new SyntaxException("@Parallel currently supports only
for-in loops",
+ annotation.getLineNumber(),
annotation.getColumnNumber()), source));
+ return;
+ }
+
+ injectParallelThreadStart(forStatement, annotation);
+ }
+
+ private static void injectParallelThreadStart(final ForStatement
forStatement, final AnnotationNode annotation) {
+ Statement originalBody = forStatement.getLoopBlock();
+
+ // Use a closure parameter with the same loop variable name and
curry(currentValue)
+ // so each launched thread receives its own iteration value.
+ Parameter loopParameter = forStatement.getValueVariable();
+ Parameter workerParameter = new
Parameter(loopParameter.getOriginType(), loopParameter.getName());
+
+ BlockStatement workerCode = block();
+ if (originalBody instanceof BlockStatement bodyBlock) {
+ bodyBlock.getStatements().forEach(workerCode::addStatement);
+ } else {
+ workerCode.addStatement(originalBody);
+ }
+
+ ClosureExpression worker = new ClosureExpression(new
Parameter[]{workerParameter}, workerCode);
+ worker.setVariableScope(new VariableScope());
+ worker.setSourcePosition(annotation);
+
+ Expression currentLoopValue = new VariableExpression(loopParameter);
+ Expression boundWorker = callX(worker, "curry",
args(currentLoopValue));
+ Expression runnable = castX(ClassHelper.make(Runnable.class),
boundWorker);
+
+ Statement startThread = stmt(callX(
+ ctorX(ClassHelper.make(Thread.class), args(runnable)),
+ "start"));
+ startThread.setSourcePosition(annotation);
+
+ BlockStatement loopBody = block(startThread);
+ loopBody.setSourcePosition(originalBody);
+ forStatement.setLoopBlock(loopBody);
+ }
+}
+
+
diff --git a/src/test-resources/core/AnnotatedLoop_01.groovy
b/src/test-resources/core/AnnotatedLoop_01.groovy
new file mode 100644
index 0000000000..35b1dd9a8f
--- /dev/null
+++ b/src/test-resources/core/AnnotatedLoop_01.groovy
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+// annotated for-in loops
+@Test1 for (i in someList) {
+ break
+}
+
+@Test1 @Test2 for (i in someList) {
+ break
+}
+
+@Test1(value=1) for (i in someList) {
+ break
+}
+
+@Test1
+@Test2
+for (i in someList) {
+ break
+}
+
+@Test1(v1=1, v2=2)
+@Test2
+for (String i in someList) {
+ break
+}
+
+// annotated classic for loops
+@Test1 for (int i = 0; i < 10; i++) {
+ break
+}
+
+@Test1 @Test2 for (int i = 0; i < 10; i++) {
+ break
+}
+
+@Test1
+@Test2(value='x')
+for (int i = 0; i < 10; i++) {
+ break
+}
+
+// annotated while loops
+@Test1 while (true) {
+ break
+}
+
+@Test1 @Test2 while (true) {
+ break
+}
+
+@Test1(value=1) while (true) {
+ break
+}
+
+@Test1
+@Test2
+while (true) {
+ break
+}
+
+@Test1(v1=1, v2=2)
+@Test2
+while (true) {
+ break
+}
+
+// annotated do-while loops
+@Test1 do {
+ break
+} while (true)
+
+@Test1 @Test2 do {
+ break
+} while (true)
+
+@Test1(value=1) do {
+ break
+} while (true)
+
+@Test1
+@Test2
+do {
+ break
+} while (true)
+
+@Test1(v1=1, v2=2)
+@Test2
+do {
+ break
+} while (true)
+
+// fully-qualified annotation on loop
[email protected] for (i in someList) {
+ break
+}
+
[email protected] while (true) {
+ break
+}
+
[email protected] do {
+ break
+} while (true)
+
+// annotation with nested parentheses
+@Test1(value=(1 + 2)) for (i in someList) {
+ break
+}
+
+@Test1(value=(1 + 2)) while (true) {
+ break
+}
+
+@Test1(value=(1 + 2)) do {
+ break
+} while (true)
diff --git a/src/test-resources/core/AnnotatedLoop_02x.groovy
b/src/test-resources/core/AnnotatedLoop_02x.groovy
new file mode 100644
index 0000000000..2f5c031b31
--- /dev/null
+++ b/src/test-resources/core/AnnotatedLoop_02x.groovy
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+import groovy.transform.ASTTest
+import org.codehaus.groovy.ast.stmt.ForStatement
+import org.codehaus.groovy.ast.stmt.WhileStatement
+import org.codehaus.groovy.ast.stmt.DoWhileStatement
+
+import static org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS
+
+// --- annotated for-in loop: verify annotation is attached ---
+@ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ assert node instanceof ForStatement
+ def annos = node.statementAnnotations
+ assert annos.size() == 1
+ assert annos[0].classNode.name == 'groovy.transform.ASTTest'
+})
+for (int i in [1, 2, 3]) {
+ assert i > 0
+}
+
+// --- annotated classic for loop: verify annotation is attached ---
+int sum = 0
+@ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ assert node instanceof ForStatement
+ def annos = node.statementAnnotations
+ assert annos.size() == 1
+ assert annos[0].classNode.name == 'groovy.transform.ASTTest'
+})
+for (int i = 0; i < 5; i++) {
+ sum += i
+}
+assert sum == 10
+
+// --- annotated while loop: verify annotation is attached ---
+int count = 0
+@ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ assert node instanceof WhileStatement
+ def annos = node.statementAnnotations
+ assert annos.size() == 1
+ assert annos[0].classNode.name == 'groovy.transform.ASTTest'
+})
+while (count < 3) {
+ count++
+}
+assert count == 3
+
+// --- annotated do-while loop: verify annotation is attached ---
+int x = 0
+@ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ assert node instanceof DoWhileStatement
+ def annos = node.statementAnnotations
+ assert annos.size() == 1
+ assert annos[0].classNode.name == 'groovy.transform.ASTTest'
+})
+do {
+ x++
+} while (x < 4)
+assert x == 4
+
+// --- multi-annotation on for loop ---
+@ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ assert node instanceof ForStatement
+ def annos = node.statementAnnotations
+ assert annos.size() == 2
+ assert annos[0].classNode.name == 'groovy.transform.ASTTest'
+ assert annos[1].classNode.name == 'java.lang.SuppressWarnings'
+})
+@SuppressWarnings('unused')
+for (String s in ['a', 'b']) {
+ assert s.length() == 1
+}
+
+// --- multi-annotation on while loop (multi-line) ---
+int y = 0
+@ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ assert node instanceof WhileStatement
+ def annos = node.statementAnnotations
+ assert annos.size() == 2
+ assert annos[0].classNode.name == 'groovy.transform.ASTTest'
+ assert annos[1].classNode.name == 'java.lang.SuppressWarnings'
+})
+@SuppressWarnings('unused')
+while (y < 2) {
+ y++
+}
+assert y == 2
+
+// --- multi-annotation on do-while loop ---
+int z = 0
+@ASTTest(phase=SEMANTIC_ANALYSIS, value={
+ assert node instanceof DoWhileStatement
+ def annos = node.statementAnnotations
+ assert annos.size() == 2
+ assert annos[0].classNode.name == 'groovy.transform.ASTTest'
+ assert annos[1].classNode.name == 'java.lang.SuppressWarnings'
+})
+@SuppressWarnings('unused')
+do {
+ z++
+} while (z < 3)
+assert z == 3
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 8dd54cf55b..1abb9bd8cb 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -285,6 +285,12 @@ final class GroovyParserTest {
doRunAndTestAntlr4('core/DoWhile_04x.groovy')
}
+ @Test
+ void 'groovy core - AnnotatedLoop'() {
+ doTest('core/AnnotatedLoop_01.groovy')
+ doRunAndTestAntlr4('core/AnnotatedLoop_02x.groovy')
+ }
+
@Test
void 'groovy core - TryCatch'() {
doTest('core/TryCatch_01.groovy')
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
index 8512cd49e2..053b89a77d 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
@@ -22,6 +22,7 @@ import
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplement
import org.apache.groovy.contracts.annotations.meta.ClassInvariant;
import
org.apache.groovy.contracts.common.impl.ClassInvariantAnnotationProcessor;
import org.apache.groovy.lang.annotation.Incubating;
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
@@ -31,13 +32,10 @@ import java.lang.annotation.Target;
/**
* <p>
- * Represents a <b>class-invariant</b>.
- * </p>
- *
- * <p>
- * The class-invariant defines assertions holding during the entire objects
life-time.
+ * Represents a <b>class-invariant</b> or a <b>loop invariant</b>.
* </p>
* <p>
+ * When applied to a class, defines assertions holding during the entire
object's life-time.
* Class-invariants are verified at runtime at the following pointcuts:
* <ul>
* <li>after a constructor call</li>
@@ -46,6 +44,17 @@ import java.lang.annotation.Target;
* </ul>
* </p>
* <p>
+ * When applied to a {@code for}, {@code while}, or {@code do-while} loop,
defines a
+ * loop invariant that is asserted at the start of each iteration:
+ * <pre>
+ * int sum = 0
+ * {@code @Invariant}({ 0 <= i && i <= 4 })
+ * for (int i in 0..4) {
+ * sum += i
+ * }
+ * </pre>
+ * </p>
+ * <p>
* Whenever a class has a parent which itself specifies a class-invariant,
that class-invariant expression is combined
* with the actual class's invariant (by using a logical AND).
* </p>
@@ -56,6 +65,7 @@ import java.lang.annotation.Target;
@ClassInvariant
@Repeatable(Invariants.class)
@AnnotationProcessorImplementation(ClassInvariantAnnotationProcessor.class)
+@GroovyASTTransformationClass("org.apache.groovy.contracts.ast.LoopInvariantASTTransformation")
public @interface Invariant {
Class value();
}
diff --git
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/LoopInvariantViolation.java
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/LoopInvariantViolation.java
new file mode 100644
index 0000000000..1543a7685f
--- /dev/null
+++
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/LoopInvariantViolation.java
@@ -0,0 +1,60 @@
+/*
+ * 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.apache.groovy.contracts;
+
+/**
+ * <p>Thrown whenever a loop invariant violation occurs.</p>
+ *
+ * @see AssertionViolation
+ * @since 6.0.0
+ */
+public class LoopInvariantViolation extends AssertionViolation {
+
+ public LoopInvariantViolation() {
+ }
+
+ public LoopInvariantViolation(Object o) {
+ super(o);
+ }
+
+ public LoopInvariantViolation(boolean b) {
+ super(b);
+ }
+
+ public LoopInvariantViolation(char c) {
+ super(c);
+ }
+
+ public LoopInvariantViolation(int i) {
+ super(i);
+ }
+
+ public LoopInvariantViolation(long l) {
+ super(l);
+ }
+
+ public LoopInvariantViolation(float f) {
+ super(f);
+ }
+
+ public LoopInvariantViolation(double d) {
+ super(d);
+ }
+}
+
diff --git
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopInvariantASTTransformation.java
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopInvariantASTTransformation.java
new file mode 100644
index 0000000000..0f06fbed3a
--- /dev/null
+++
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopInvariantASTTransformation.java
@@ -0,0 +1,109 @@
+/*
+ * 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.apache.groovy.contracts.ast;
+
+import groovy.contracts.Invariant;
+import org.apache.groovy.contracts.LoopInvariantViolation;
+import org.apache.groovy.contracts.generation.AssertStatementCreationUtility;
+import org.apache.groovy.contracts.generation.TryCatchBlockGenerator;
+import org.apache.groovy.contracts.util.ExpressionUtils;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.expr.BooleanExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.LoopingStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.transform.ASTTransformation;
+import org.codehaus.groovy.transform.GroovyASTTransformation;
+
+import java.util.List;
+
+/**
+ * Handles {@link Invariant} annotations placed on loop statements ({@code
for},
+ * {@code while}, {@code do-while}). The invariant closure is evaluated as an
+ * assertion at the start of each loop iteration.
+ * <p>
+ * When {@code @Invariant} is placed on a class (its original usage), this
+ * transform returns immediately, letting the existing global contract pipeline
+ * handle it.
+ * <p>
+ * Example:
+ * <pre>
+ * int sum = 0
+ * {@code @Invariant}({ 0 <= i && i <= 4 })
+ * for (int i in 0..4) {
+ * sum += i
+ * }
+ * </pre>
+ *
+ * @since 6.0.0
+ * @see Invariant
+ * @see org.apache.groovy.contracts.LoopInvariantViolation
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class LoopInvariantASTTransformation implements ASTTransformation {
+
+ @Override
+ public void visit(final ASTNode[] nodes, final SourceUnit source) {
+ if (nodes.length != 2) return;
+ if (!(nodes[0] instanceof AnnotationNode annotation)) return;
+
+ // Only handle loop statements; class-level @Invariant is handled by
the
+ // existing GContractsASTTransformation global pipeline.
+ if (!(nodes[1] instanceof LoopingStatement loopStatement)) return;
+
+ Expression value = annotation.getMember("value");
+ if (!(value instanceof ClosureExpression closureExpression)) return;
+
+ List<BooleanExpression> booleanExpressions =
ExpressionUtils.getBooleanExpression(closureExpression);
+ if (booleanExpressions == null || booleanExpressions.isEmpty()) return;
+
+ BlockStatement assertStatements =
AssertStatementCreationUtility.getAssertionStatements(booleanExpressions);
+
+ // Wrap in a try-catch that converts PowerAssertionError into
+ // LoopInvariantViolation with a helpful message.
+ BlockStatement wrapped =
TryCatchBlockGenerator.generateTryCatchBlockForInlineMode(
+ ClassHelper.makeWithoutCaching(LoopInvariantViolation.class),
+ "<" + Invariant.class.getName() + "> loop invariant \n\n",
+ assertStatements
+ );
+ wrapped.setSourcePosition(annotation);
+
+ injectAtLoopBodyStart(loopStatement, wrapped);
+ }
+
+ private static void injectAtLoopBodyStart(LoopingStatement loopStatement,
Statement check) {
+ Statement loopBody = loopStatement.getLoopBlock();
+ if (loopBody instanceof BlockStatement block) {
+ block.getStatements().addAll(0, ((BlockStatement)
check).getStatements());
+ } else {
+ BlockStatement newBody = new BlockStatement();
+ newBody.addStatements(((BlockStatement) check).getStatements());
+ newBody.addStatement(loopBody);
+ newBody.setSourcePosition(loopBody);
+ loopStatement.setLoopBlock(newBody);
+ }
+ }
+}
+
diff --git
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/domain/LoopInvariant.java
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/domain/LoopInvariant.java
new file mode 100644
index 0000000000..4d3cdcc46c
--- /dev/null
+++
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/domain/LoopInvariant.java
@@ -0,0 +1,44 @@
+/*
+ * 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.apache.groovy.contracts.domain;
+
+import org.codehaus.groovy.ast.expr.BooleanExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+
+/**
+ * <p>A loop-invariant assertion that must hold at the start of each
iteration.</p>
+ *
+ * @since 6.0.0
+ */
+public class LoopInvariant extends Assertion<LoopInvariant> {
+
+ public static final LoopInvariant DEFAULT = new LoopInvariant(block(),
boolX(constX(true)));
+
+ public LoopInvariant() {
+ }
+
+ public LoopInvariant(BlockStatement blockStatement, BooleanExpression
booleanExpression) {
+ super(blockStatement, booleanExpression);
+ }
+}
+
diff --git a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
index 58e7f5697f..2fe98b130c 100644
--- a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
+++ b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
@@ -22,7 +22,8 @@
= Groovy Contracts – design by contract support for Groovy
This module provides contract annotations that support the specification of
class-invariants,
-as well as pre- and post-conditions on Groovy classes and interfaces.
+pre- and post-conditions on Groovy classes and interfaces, and loop invariants
on
+`for`, `while`, and `do-while` loops.
Special support is provided so that post-conditions may refer to the old value
of variables
or to the result value associated with calling a method.
@@ -42,6 +43,7 @@
include::../test/ContractsTest.groovy[tags=basic_example,indent=0]
Groovy contracts supports the following feature set:
* definition of class invariants, pre- and post-conditions via @Invariant,
@Requires and @Ensures
+* definition of loop invariants on `for`, `while`, and `do-while` loops via
@Invariant
* inheritance of class invariants, pre- and post-conditions of concrete
predecessor classes
* inheritance of class invariants, pre- and post-conditions in implemented
interfaces
* usage of old and result variable in post-condition assertions
@@ -95,3 +97,46 @@
include::../test/ContractsTest.groovy[tags=jep445_example,indent=0]
You could even place an `@Invariant` annotation on the `main` method
and it will be moved to the generated script class.
+
+== Loop Invariants
+
+In addition to class-level invariants, `@Invariant` can be placed directly on
+`for`, `while`, and `do-while` loops. A loop invariant is a condition that must
+hold at the start of each iteration. If the condition is violated, a
+`LoopInvariantViolation` (a subclass of `AssertionError`) is thrown.
+
+This is inspired by design-by-contract constructs found in languages like Dafny
+and Eiffel.
+
+=== For loop
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=loop_invariant_for_example,indent=0]
+----
+
+=== While loop
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=loop_invariant_while_example,indent=0]
+----
+
+=== Multiple invariants
+
+Multiple `@Invariant` annotations can be stacked on a single loop, and each
+condition is checked independently at the start of every iteration:
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=loop_invariant_multiple_example,indent=0]
+----
+
+=== Rules for loop invariant closures
+
+* The closure should be a boolean expression (or multiple expressions).
+* The closure may reference any variables visible in the enclosing scope,
+ including the loop variable.
+* Assignment operators and state-changing postfix/prefix operators are
+ not supported inside the closure.
+
diff --git a/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
b/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
index 6f10a0371f..18aa43eeb0 100644
--- a/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
+++ b/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
@@ -110,6 +110,52 @@ class ContractsTest extends GroovyTestCase {
'''
}
+ void testLoopInvariantForIn() {
+ assertScript '''
+ // tag::loop_invariant_for_example[]
+ import groovy.contracts.Invariant
+
+ int sum = 0
+ @Invariant({ 0 <= i && i <= 4 })
+ for (int i in 0..4) {
+ sum += i
+ }
+ assert sum == 10
+ // end::loop_invariant_for_example[]
+ '''
+ }
+
+ void testLoopInvariantWhile() {
+ assertScript '''
+ // tag::loop_invariant_while_example[]
+ import groovy.contracts.Invariant
+
+ int n = 10
+ @Invariant({ n >= 0 })
+ while (n > 0) {
+ n--
+ }
+ assert n == 0
+ // end::loop_invariant_while_example[]
+ '''
+ }
+
+ void testLoopInvariantMultiple() {
+ assertScript '''
+ // tag::loop_invariant_multiple_example[]
+ import groovy.contracts.Invariant
+
+ int sum = 0
+ @Invariant({ sum >= 0 })
+ @Invariant({ sum <= 100 })
+ for (int i in 1..5) {
+ sum += i
+ }
+ assert sum == 15
+ // end::loop_invariant_multiple_example[]
+ '''
+ }
+
void testJep445Script() {
runScript '''
// tag::jep445_example[]
diff --git
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopInvariantTests.groovy
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopInvariantTests.groovy
new file mode 100644
index 0000000000..3ac1f77565
--- /dev/null
+++
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopInvariantTests.groovy
@@ -0,0 +1,159 @@
+/*
+ * 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.apache.groovy.contracts.tests.inv
+
+import org.apache.groovy.contracts.tests.basic.BaseTestClass
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Tests for {@code @Invariant} applied to loop statements.
+ */
+class LoopInvariantTests extends BaseTestClass {
+
+ @Test
+ void invariantOnForInLoop() {
+ assertScript '''
+ import groovy.contracts.Invariant
+
+ int sum = 0
+ @Invariant({ 0 <= i && i <= 4 })
+ for (int i in 0..4) {
+ sum += i
+ }
+ assert sum == 10
+ '''
+ }
+
+ @Test
+ void invariantOnClassicForLoop() {
+ assertScript '''
+ import groovy.contracts.Invariant
+
+ int product = 1
+ @Invariant({ product >= 1 })
+ for (int i = 1; i <= 5; i++) {
+ product *= i
+ }
+ assert product == 120
+ '''
+ }
+
+ @Test
+ void invariantOnWhileLoop() {
+ assertScript '''
+ import groovy.contracts.Invariant
+
+ int n = 10
+ @Invariant({ n >= 0 })
+ while (n > 0) {
+ n--
+ }
+ assert n == 0
+ '''
+ }
+
+ @Test
+ void invariantOnDoWhileLoop() {
+ assertScript '''
+ import groovy.contracts.Invariant
+
+ int count = 0
+ @Invariant({ count >= 0 })
+ do {
+ count++
+ } while (count < 3)
+ assert count == 3
+ '''
+ }
+
+ @Test
+ void multipleInvariantsOnLoop() {
+ assertScript '''
+ import groovy.contracts.Invariant
+
+ int sum = 0
+ @Invariant({ sum >= 0 })
+ @Invariant({ sum <= 100 })
+ for (int i in 1..5) {
+ sum += i
+ }
+ assert sum == 15
+ '''
+ }
+
+ @Test
+ void invariantViolationThrows() {
+ shouldFail AssertionError, '''
+ import groovy.contracts.Invariant
+
+ int n = 5
+ @Invariant({ n > 0 })
+ while (n >= 0) {
+ n--
+ }
+ '''
+ }
+
+ @Test
+ void invariantWithComplexExpression() {
+ assertScript '''
+ import groovy.contracts.Invariant
+
+ def items = []
+ @Invariant({ items.size() <= 5 })
+ for (int i in 1..5) {
+ items << i
+ }
+ assert items == [1, 2, 3, 4, 5]
+ '''
+ }
+
+ @Test
+ void invariantViolationOnFirstIteration() {
+ shouldFail AssertionError, '''
+ import groovy.contracts.Invariant
+
+ int x = -1
+ @Invariant({ x >= 0 })
+ for (int i in 0..2) {
+ x = i
+ }
+ '''
+ }
+
+ @Test
+ void classInvariantStillWorksWithLoopInvariantTransform() {
+ assertScript '''
+ import groovy.contracts.Invariant
+
+ @Invariant({ property != null })
+ class Foo {
+ def property
+ Foo(val) { property = val }
+ }
+
+ def f = new Foo('hello')
+ assert f.property == 'hello'
+ '''
+ }
+}
+