This is an automated email from the ASF dual-hosted git repository.

paulk-asert 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 42ce7e0961 GROOVY-12038: Graduate groovy-contracts from incubating to 
stable
42ce7e0961 is described below

commit 42ce7e09611dd02d85d6d1e80e3c079a90100460
Author: Paul King <[email protected]>
AuthorDate: Sun May 24 20:28:42 2026 +1000

    GROOVY-12038: Graduate groovy-contracts from incubating to stable
---
 .../src/main/java/groovy/contracts/Contracted.java |  2 +
 .../src/main/java/groovy/contracts/Ensures.java    |  4 +-
 .../java/groovy/contracts/EnsuresConditions.java   | 10 ++-
 .../src/main/java/groovy/contracts/Invariant.java  |  4 +-
 .../src/main/java/groovy/contracts/Invariants.java | 10 ++-
 .../src/main/java/groovy/contracts/Requires.java   |  4 +-
 .../java/groovy/contracts/RequiresConditions.java  | 10 ++-
 .../main/java/groovy/contracts/package-info.java   | 23 ++++-
 .../src/spec/doc/contracts-userguide.adoc          | 97 +++++++++++++++++++++-
 .../src/spec/test/ContractsTest.groovy             | 50 +++++++++++
 10 files changed, 191 insertions(+), 23 deletions(-)

diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java
index 06204f1ffc..5eda698b91 100644
--- 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java
+++ 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java
@@ -35,6 +35,8 @@ import java.lang.annotation.Target;
  *
  * import groovy.contracts.*
  * </pre>
+ *
+ * @since 4.0.0
  */
 @Documented
 @Target({ElementType.PACKAGE, ElementType.TYPE})
diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java
index 5ea65e790f..d79144ca73 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java
@@ -21,7 +21,6 @@ package groovy.contracts;
 import 
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplementation;
 import org.apache.groovy.contracts.annotations.meta.Postcondition;
 import org.apache.groovy.contracts.common.impl.EnsuresAnnotationProcessor;
-import org.apache.groovy.lang.annotation.Incubating;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
@@ -77,11 +76,12 @@ import java.lang.annotation.Target;
  *   }
  * </pre>
  * </p>
+ *
+ * @since 4.0.0
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
 @Postcondition
 @Repeatable(EnsuresConditions.class)
 @AnnotationProcessorImplementation(EnsuresAnnotationProcessor.class)
diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
index b89d8cebe2..a46c94f60b 100644
--- 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
+++ 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
@@ -18,8 +18,6 @@
  */
 package groovy.contracts;
 
-import org.apache.groovy.lang.annotation.Incubating;
-
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -27,12 +25,16 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Represents multiple postconditions.
+ * Container annotation for multiple {@link Ensures} postconditions on the 
same target.
+ * Synthesized by the compiler from stacked {@code @Ensures} annotations via
+ * {@link java.lang.annotation.Repeatable}; not normally written by hand.
+ *
+ * @since 5.0.0
+ * @see Ensures
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
 public @interface EnsuresConditions {
     /**
      * Returns the repeated postcondition annotations declared on the target 
element.
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 96760e920f..c89bc4c813 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
@@ -23,7 +23,6 @@ import groovy.lang.annotation.ExtendedTarget;
 import 
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplementation;
 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.Documented;
@@ -61,11 +60,12 @@ import java.lang.annotation.Target;
  * 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>
+ *
+ * @since 4.0.0
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
-@Incubating
 @ClassInvariant
 @Repeatable(Invariants.class)
 @ExtendedTarget({ExtendedElementType.LOOP, ExtendedElementType.IMPORT})
diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java
index eaa81f4a3f..0817e08932 100644
--- 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java
+++ 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java
@@ -18,8 +18,6 @@
  */
 package groovy.contracts;
 
-import org.apache.groovy.lang.annotation.Incubating;
-
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -27,12 +25,16 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Represents multiple invariants
+ * Container annotation for multiple {@link Invariant} declarations on the 
same type.
+ * Synthesized by the compiler from stacked {@code @Invariant} annotations via
+ * {@link java.lang.annotation.Repeatable}; not normally written by hand.
+ *
+ * @since 5.0.0
+ * @see Invariant
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
-@Incubating
 public @interface Invariants {
     /**
      * Returns the repeated invariant annotations declared on the annotated 
type.
diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java
index e1da332744..0cc9139dde 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java
@@ -21,7 +21,6 @@ package groovy.contracts;
 import 
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplementation;
 import org.apache.groovy.contracts.annotations.meta.Precondition;
 import org.apache.groovy.contracts.common.impl.RequiresAnnotationProcessor;
-import org.apache.groovy.lang.annotation.Incubating;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
@@ -54,11 +53,12 @@ import java.lang.annotation.Target;
  *   }
  * </pre>
  * </p>
+ *
+ * @since 4.0.0
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
 @Precondition
 @AnnotationProcessorImplementation(RequiresAnnotationProcessor.class)
 @Repeatable(RequiresConditions.class)
diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
index ac742430e2..fedd0e502c 100644
--- 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
+++ 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
@@ -18,8 +18,6 @@
  */
 package groovy.contracts;
 
-import org.apache.groovy.lang.annotation.Incubating;
-
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -27,12 +25,16 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 /**
- * Represents multiple preconditions.
+ * Container annotation for multiple {@link Requires} preconditions on the 
same target.
+ * Synthesized by the compiler from stacked {@code @Requires} annotations via
+ * {@link java.lang.annotation.Repeatable}; not normally written by hand.
+ *
+ * @since 5.0.0
+ * @see Requires
  */
 @Documented
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
 public @interface RequiresConditions {
     /**
      * Returns the repeated precondition annotations declared on the target 
element.
diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java
index b976ba4da3..b66fa2f882 100644
--- 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java
+++ 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java
@@ -21,10 +21,25 @@
  * Design-by-contract (DbC) support with compile-time assertion generation.
  *
  * <p>
- * {@link groovy.contracts.Requires @Requires} (precondition),
- * {@link groovy.contracts.Ensures @Ensures} (postcondition),
- * {@link groovy.contracts.Invariant @Invariant} (class invariant).
- * Postconditions can access {@code result} and {@code old()} values.
+ * Stable annotations:
  * </p>
+ * <ul>
+ *   <li>{@link groovy.contracts.Requires @Requires} &ndash; method 
precondition</li>
+ *   <li>{@link groovy.contracts.Ensures @Ensures} &ndash; method 
postcondition; closures
+ *       may reference {@code result} and {@code old}</li>
+ *   <li>{@link groovy.contracts.Invariant @Invariant} &ndash; class 
invariant, also usable
+ *       as a loop invariant on {@code for} / {@code while} / {@code do-while} 
loops</li>
+ *   <li>{@link groovy.contracts.Contracted @Contracted} &ndash; package- or 
class-level
+ *       opt-in marker that enables contract processing</li>
+ * </ul>
+ * <p>
+ * Incubating in 6.0.0 (semantics may evolve in a subsequent 6.x release):
+ * </p>
+ * <ul>
+ *   <li>{@link groovy.contracts.Modifies @Modifies} &ndash; frame condition 
declaring
+ *       which fields and parameters a method may modify</li>
+ *   <li>{@link groovy.contracts.Decreases @Decreases} &ndash; loop 
termination measure
+ *       (a strictly-decreasing, non-negative variant)</li>
+ * </ul>
  */
 package groovy.contracts;
diff --git a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc 
b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
index dee4c8934e..1aa1100641 100644
--- a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
+++ b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
@@ -38,6 +38,37 @@ package.
 include::../test/ContractsTest.groovy[tags=basic_example,indent=0]
 ----
 
+== Enabling contracts with @Contracted
+
+Contract processing is opt-in. By default, classes that simply mention the 
contract annotations
+will have them processed; the `@Contracted` marker is used when you want to 
make the opt-in
+explicit at the package level, or when you want to apply contract processing 
to a type that
+doesn't itself carry any contract annotations (for example, an empty class 
whose contract
+comes entirely from inherited interfaces).
+
+`@Contracted` may be placed on a package declaration (in a 
`package-info.groovy` /
+`package-info.java`) to enable contract processing across every type in that 
package:
+
+[source,groovy]
+----
+@Contracted
+package com.acme.banking
+
+import groovy.contracts.*
+----
+
+or on an individual type:
+
+[source,groovy]
+----
+@Contracted
+class Account { /* ... */ }
+----
+
+Once enabled, runtime assertion checking still honours the JVM's `-ea` / `-da` 
flags;
+`@Contracted` controls whether the AST transforms generate assertion code at 
compile time,
+not whether the resulting assertions execute at run time.
+
 == More Features
 
 Groovy contracts supports the following feature set:
@@ -45,12 +76,13 @@ 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
 * declaration of frame conditions (which fields a method may modify) via 
@Modifies
+* declaration of loop termination measures via @Decreases
 * 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
 * assertion injection in Plain Old Groovy Objects (POGOs)
 * human-readable assertion messages, based on Groovy power asserts
-* enabling contracts at package- or class-level with @AssertionsEnabled
+* enabling contracts at package- or class-level with @Contracted
 * enable or disable contract checking with Java's -ea and -da VM parameters
 * annotation contracts: a way to reuse reappearing contract elements in a 
project domain model
 * detection of circular assertion method calls
@@ -141,6 +173,69 @@ 
include::../test/ContractsTest.groovy[tags=loop_invariant_multiple_example,inden
 * Assignment operators and state-changing postfix/prefix operators are
   not supported inside the closure.
 
+== Loop Termination with @Decreases
+
+Where `@Invariant` on a loop asserts a condition that must hold _during_ 
iteration,
+`@Decreases` asserts a condition that guarantees the loop will _terminate_: a
+_loop variant_, also known as a termination measure. The closure must yield a 
value
+whose successive iteration values form a strictly decreasing, well-founded 
sequence.
+Two value shapes are supported:
+
+* A single `Comparable` value &mdash; usually numeric &mdash; that strictly 
decreases
+  on every iteration. If the value is also a `Number`, it must additionally 
remain
+  non-negative.
+* A `List` of `Comparable` elements, compared lexicographically: the first 
position
+  at which consecutive values differ must show a strict decrease, and earlier
+  positions must be equal. This is the natural way to express termination for
+  nested loops (e.g.&nbsp;`[i, j]`).
+
+If the variant fails to decrease (or a numeric scalar variant becomes 
negative),
+a `LoopVariantViolation` (a subclass of `AssertionError`) is thrown.
+
+This is inspired by similar constructs in design-by-contract languages such as
+Dafny and Eiffel.
+
+NOTE: `@Decreases` is incubating in Groovy 6.0.0. The contract&nbsp;&mdash; in 
particular
+the scalar non-negativity rule and the lexicographic shape for `List` 
variants&nbsp;&mdash;
+may be relaxed or extended in a future release based on usage feedback.
+
+=== While loop
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=decreases_while_example,indent=0]
+----
+
+=== For loop
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=decreases_for_example,indent=0]
+----
+
+=== Lexicographic (tuple) variants
+
+For nested loops where no single counter strictly decreases on every iteration,
+return a `List` whose elements form a tuple compared lexicographically. In the
+example below the outer counter stays equal while the inner one decreases, then
+the outer one decreases and the inner one is allowed to reset:
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=decreases_lexicographic_example,indent=0]
+----
+
+=== Rules for @Decreases closures
+
+* The closure must return a `Comparable` value or a `List` of `Comparable` 
elements.
+* The value is evaluated at the start and end of each iteration; the 
end-of-iteration
+  value must be strictly less than the start-of-iteration value 
(lexicographically,
+  for `List` variants).
+* For numeric scalar variants, the value must also remain `>= 0`.
+  This non-negativity rule does _not_ apply to non-`Number` `Comparable` values
+  (e.g.&nbsp;`String`) or to `List` variants.
+* The closure may reference any variables visible in the enclosing scope.
+
 == Frame Conditions with @Modifies
 
 The `@Modifies` annotation declares which fields or parameters a method is 
allowed to modify.
diff --git a/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy 
b/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
index 18aa43eeb0..06343cc061 100644
--- a/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
+++ b/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
@@ -156,6 +156,56 @@ class ContractsTest extends GroovyTestCase {
         '''
     }
 
+    void testDecreasesWhile() {
+        assertScript '''
+        // tag::decreases_while_example[]
+        import groovy.contracts.Decreases
+
+        int n = 10
+        @Decreases({ n })
+        while (n > 0) {
+            n--
+        }
+        assert n == 0
+        // end::decreases_while_example[]
+        '''
+    }
+
+    void testDecreasesFor() {
+        assertScript '''
+        // tag::decreases_for_example[]
+        import groovy.contracts.Decreases
+
+        int remaining = 5
+        @Decreases({ remaining })
+        for (int i = 0; i < 5; i++) {
+            remaining--
+        }
+        assert remaining == 0
+        // end::decreases_for_example[]
+        '''
+    }
+
+    void testDecreasesLexicographic() {
+        assertScript '''
+        // tag::decreases_lexicographic_example[]
+        import groovy.contracts.Decreases
+
+        int outer = 2, inner = 3
+        @Decreases({ [outer, inner] })
+        while (outer > 0) {
+            if (inner > 0) {
+                inner--
+            } else {
+                outer--
+                inner = 3
+            }
+        }
+        assert outer == 0
+        // end::decreases_lexicographic_example[]
+        '''
+    }
+
     void testJep445Script() {
         runScript '''
         // tag::jep445_example[]

Reply via email to