Skip to content

Commit 4890b75

Browse files
committed
tweak wording
1 parent 1b3fe51 commit 4890b75

1 file changed

Lines changed: 165 additions & 8 deletions

File tree

subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc

Lines changed: 165 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ifndef::reldir_typecheckers[]
2222
:reldir_typecheckers: .
2323
endif::[]
24+
:icons: font
2425
2526
= Built-in auxiliary type checkers
2627
@@ -30,15 +31,146 @@ Groovy's static nature includes an extensible type-checking mechanism.
3031
This mechanism allows users to selectively strengthen or weaken type checking
3132
as needed to cater for scenarios where the standard type checking isn't sufficient.
3233
33-
In addition to allowing you to write your own customer checkers,
34-
Groovy offers a handful of useful built-in type checkers.
35-
These provide additional checks for specific scenarios.
34+
In addition to allowing you to write your own custom checkers,
35+
Groovy offers a suite of built-in type checkers that provide additional
36+
compile-time verification for specific concerns:
3637
37-
In the examples which follow, we'll explicitly show declaring
38-
the type checker we want to use.
39-
As usual, Groovy's compiler customization mechanisms would allow you to
40-
simplify application of such checkers, e.g. make it apply globally
41-
using a compiler configuration script, as just one example.
38+
[cols="3,10",options="header"]
39+
|===
40+
| Checker | What it does
41+
42+
| <<Checking Regular Expressions,RegexChecker>>
43+
a| Validates regex patterns at compile time -- no more `PatternSyntaxException` at runtime
44+
45+
[listing,subs="+quotes,macros"]
46+
----
47+
assert 'Apple' =~ /(a\|A).*/ [.green]#// icon:check[] okay#
48+
assert 'apple' =~ /(a\|A.*/ [.red]#// icon:times[] compile error: unclosed group#
49+
----
50+
51+
| <<Checking Format Strings,FormatStringChecker>>
52+
a| Validates `printf`/`format` specifiers against argument types -- catches mismatches at compile time
53+
54+
[listing,subs="+quotes,macros"]
55+
----
56+
String.format('%d: %s', 42, 'ok') [.green]#// icon:check[] okay#
57+
String.format('%d: %s', 'x', 42) [.red]#// icon:times[] IllegalFormatConversion: d != String#
58+
----
59+
60+
| <<Checking Null Safety (Incubating),NullChecker>>
61+
a| Detects potential null dereferences using annotations and optional flow analysis
62+
63+
[listing,subs="+quotes,macros"]
64+
----
65+
if (name != null) name.size() [.green]#// icon:check[] null guard#
66+
name?.size() [.green]#// icon:check[] safe navigation#
67+
name.size() [.red]#// icon:times[] potential null dereference#
68+
----
69+
70+
| <<Checking @Modifies Frame Conditions (Incubating),ModifiesChecker>>
71+
a| Verifies methods only modify declared fields -- humans and AI know what changed and what didn't
72+
73+
[listing,subs="+quotes,macros"]
74+
----
75+
@Modifies({ this.balance })
76+
void deposit(n) { balance += n } [.green]#// icon:check[] balance is declared#
77+
78+
@Modifies({ })
79+
void withdraw(n) { balance -= n } [.red]#// icon:times[] balance not in @Modifies#
80+
----
81+
82+
| <<Checking @Pure Purity (Incubating),PurityChecker>>
83+
a| Enforces that `@Pure` methods have no side effects, with configurable strictness
84+
85+
[listing,subs="+quotes,macros"]
86+
----
87+
@Pure int twice() { value * 2 } [.green]#// icon:check[] reads only#
88+
@Pure int bad() { count++ } [.red]#// icon:times[] field mutation#
89+
----
90+
|===
91+
92+
These checkers work with annotations from multiple libraries (JSpecify, JetBrains,
93+
Checker Framework, and others) -- matching by simple name so you can use whichever
94+
annotation library your project already depends on.
95+
96+
=== Why these checkers matter
97+
98+
In a world which seems to be having an ever-increasing focus on AI,
99+
here are numerous reasons you might want to make liberal use of checkers and their annotations:
100+
101+
* The checker declarations and associated annotations, along with other Groovy annotations
102+
used with AST transforms, e.g. `@Invariant` from `groovy-contracts`, form a declarative
103+
specification of program behavior. This specification replaces "read the body" reasoning allowing humans and AI to understand system behavior at scale.
104+
* When an AI agent generates or modifies code, no human may fully understand the result. Type checkers act as a compile-time safety net on what those programs can do.
105+
* If we can treat our systems as composable black boxes each with well specified behavior we can more easily start to apply compositional reasoning to our systems.
106+
* Groovy's dynamic features (metaprogramming, runtime dispatch, invokeMethod) are powerful
107+
but can make static reasoning hard. AI models can hallucinate about what dynamic code does.
108+
Type checkers let teams selectively fill in the missing gaps in static reasoning,
109+
that AI would otherwise struggle with.
110+
* As AI agents increasingly write, review, and deploy code autonomously, we need machine-checkable invariants which form trust boundaries in agentic workflows.
111+
112+
As an example, let's explore how combining some checkers and design-by-contract annotations
113+
allow both humans and AI to reason about method behavior
114+
without reading method implementations. Consider this annotated class:
115+
116+
[source,groovy]
117+
----
118+
@Invariant({ balance >= 0 })
119+
class Account {
120+
BigDecimal balance = 0
121+
List<String> log = []
122+
123+
@Requires({ amount > 0 })
124+
@Ensures({ balance == old.balance + amount })
125+
@Modifies({ [this.balance, this.log] })
126+
void deposit(BigDecimal amount) {
127+
balance += amount
128+
log.add("deposit $amount")
129+
}
130+
131+
@Requires({ amount > 0 && amount <= balance })
132+
@Ensures({ balance == old.balance - amount })
133+
@Modifies({ [this.balance, this.log] })
134+
void withdraw(BigDecimal amount) {
135+
balance -= amount
136+
log.add("withdraw $amount")
137+
}
138+
139+
@Pure
140+
BigDecimal available() { balance }
141+
}
142+
----
143+
144+
When analyzing a sequence of calls:
145+
146+
[source,groovy]
147+
----
148+
account.deposit(100)
149+
account.withdraw(30)
150+
def bal = account.available()
151+
----
152+
153+
*With annotations*, each call is a self-contained specification -- 3 linear reasoning steps:
154+
155+
1. `deposit(100)`: `@Requires` met (100 > 0), `@Ensures` gives `balance == old + 100`,
156+
`@Modifies` proves only `balance` and `log` changed
157+
2. `withdraw(30)`: `@Requires` met (30 > 0, 30 <= 100), `@Ensures` gives `balance == 100 - 30 = 70`,
158+
`@Modifies` proves `withdraw` didn't undo the deposit
159+
3. `available()`: `@Pure` proves no side effects -- just returns `balance` (70)
160+
161+
This is human and AI reasoning that scales!
162+
163+
*Without annotations*, the analyzer must read every method body, verify what each one
164+
modifies (2 fields × 3 calls = 6 "did this change?" questions), re-verify earlier state
165+
after later calls, and check whether `available()` has hidden side effects. This grows
166+
as _O(fields × calls × call_depth)_.
167+
168+
=== Using the checkers
169+
170+
In the examples which follow, we explicitly show declaring the type checker to use.
171+
Groovy's compiler customization mechanisms allow you to simplify application of
172+
such checkers, e.g. make it apply globally using a compiler configuration script,
173+
as just one example.
42174
43175
== Checking Regular Expressions
44176
@@ -375,6 +507,13 @@ Null pointer dereferences are one of the most common sources of runtime errors.
375507
The `NullChecker` performs compile-time null-safety analysis, detecting potential
376508
null dereferences and null-safety violations before code is executed.
377509
510+
Languages like Kotlin and Swift distinguish nullable and non-nullable types throughout
511+
the type system, eliminating null errors by construction. Groovy takes a more pragmatic
512+
approach: flow analysis can catch many null issues without annotations (in `strict` mode),
513+
and `@Nullable`/`@NonNull` annotations provide additional precision where needed.
514+
This lets you adopt null safety incrementally -- annotate the critical boundaries first,
515+
and let flow analysis handle the rest.
516+
378517
Two modes are provided:
379518
380519
* `NullChecker` — *Annotation-based*: Checks code annotated with `@Nullable` and
@@ -674,6 +813,13 @@ condition declarations from the `groovy-contracts` module. It checks that method
674813
modify the fields and parameters they declare, helping both humans and AI reason about
675814
what a method changes -- and crucially, what it does not.
676815
816+
Frame conditions are a well-established concept in formal verification. Dafny's `modifies`
817+
clause and JML's `assignable` clause serve the same purpose -- declaring what a method may
818+
change so that everything else is known to be preserved. Groovy's `@Modifies` brings this
819+
idea to the JDK ecosystem in a pragmatic, opt-in form. For projects already using JetBrains
820+
annotations, the checker also recognises `@Contract(mutates = "...")` which expresses
821+
similar semantics.
822+
677823
This checker is opt-in and only activates for methods annotated with `@Modifies`:
678824
679825
[source,groovy]
@@ -830,6 +976,17 @@ method bodies -- each method becomes a self-contained specification.
830976
The `PurityChecker` verifies that `@Pure` methods have no side effects. By default, strict
831977
purity is enforced. The `allows` option declares which effect categories are tolerated.
832978
979+
Languages like Haskell enforce purity through the type system -- all side effects must be
980+
expressed via IO and State monads, making impure code structurally impossible. Frege (a
981+
Haskell dialect for the JVM) takes the same approach. This is powerful but requires
982+
incorporating these patterns into your application's type hierarchies, which can be
983+
impractical when integrating with JDK libraries that don't follow these conventions.
984+
985+
Groovy's `PurityChecker` provides an opt-in way to achieve similar levels of reasoning about
986+
method purity while staying pragmatic about the JDK ecosystem. The `allows` option
987+
acknowledges that real-world "pure" methods sometimes log or emit metrics -- effects that
988+
don't affect program correctness but would violate strict Haskell-style purity.
989+
833990
=== Basic usage
834991
835992
[source,groovy]

0 commit comments

Comments
 (0)