[ 
https://issues.apache.org/jira/browse/GROOVY-12468?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12468:
-------------------------------
    Description: 
_This is a spike of a feature several other languages are experimenting 
with/supporting. It is essentially an annotation that records a comment (not 
unlike @Groovydoc) but can have special meaning for tooling - allowing 
"hole-driven" development._

h2. Summary

Add a {{@Spec}} annotation that marks a method (and optionally a loop or local 
declaration) as having a prose specification intended as a human (think pair 
programming/TDD) or LLM prompt, alongside the method's typed signature and any 
{{@Requires}}/{{@Ensures}} contracts. A method carrying {{@Spec}} with an empty 
body is an unfilled "hole". A human or tool (outside the compiler) fills the 
hole ahead of time by writing a generated body into the source under the spec, 
and the spec stays in place as documentation and as the input for later 
regeneration by an agent or validation by a human reviewer.

The annotation is essentially a structured comment: unlike a plain comment, 
tools can find it, and the compiler can tell a filled body from an unfilled one.

h2. Motivation

Martin Odersky's Scala Days 2026 keynote showed program holes filled ahead of 
time: a method with a typed signature, a prose spec string and {{???}}, and a 
"sync" step where an agent writes the body under the spec. Groovy can offer the 
same workflow with a stronger guarantee, because a fill can be checked against 
contracts, not just against types.

* The spec is written for both people and models, and it lives next to the code 
it describes.
* Contracts make the spec checkable. For example, the keynote's own generated 
date parser accepts 31 February; a one-line {{@Ensures}} catches it.
* No runtime evaluation, in-process compilation or typed binding is needed: the 
generated body sits in real source and the compiler sees its real scope.
* Writing contracts for this example already found GROOVY-12463 (contracts 
skipped on a trailing implicit return after an early {{return}}).

h2. Example

{code:groovy}
import groovy.contracts.Ensures
import groovy.contracts.Requires

class Dates {
    @Spec('''
        Parse an ISO-8601 calendar date (yyyy-MM-dd) into its year, month
        and day. Reject dates that do not exist, such as 2026-02-31.
    ''')
    @Requires({ text != null })
    @Ensures({ result.month in 1..12 && result.day <= YearMonth.of(result.year, 
result.month).lengthOfMonth() })
    static DateParts parse(String text) { }   // unfilled: throws until synced
}
{code}

After a sync, the body is filled in and the {{@Spec}} remains, with a {{fill}} 
member recording what was generated (see below).

h2. Proposal

h3. 1. The annotation

* {{@Spec(String value)}}: the prose specification.
* Target: methods; also {{@ExtendedTarget(ExtendedElementType.LOOP)}} and local 
variable declarations for smaller holes. Probes on 6.0.0 confirm the annotation 
can be placed on {{for}}, {{while}} and {{do-while}} loops and on local 
declarations.
* {{fill}} member (optional, written by tooling): a hash of the spec text and 
of the generated body, so a tool can tell whether the spec or the body has 
changed since the last fill.
* Source/class retention to be decided. Loop and local annotations are not 
written to the class file in any case, so those specs exist only in source.

h3. 2. A small local transform

* A method carrying {{@Spec}} with an empty body is compiled to throw (for 
example {{UnsupportedOperationException("unfilled @Spec: Dates.parse")}}) 
instead of returning {{null}}. An unfilled loop can be made to throw similarly.
* The transform never calls a model. Compiling still runs nothing beyond what 
it runs today.

h3. 3. The sync tool (outside core)

Not part of this ticket's compiler change, but it defines the contract the 
annotation must support. A library with a command line (and possibly an MCP 
tool) that IDE plugins and CI call rather than reimplement:

* Find unfilled specs; prompt the model with the prose, contracts, signature 
and types in scope.
* Compile each candidate with the configured type checkers, check contracts (by 
proof via groovy-verify where possible, otherwise by tests), and retry on 
diagnostics or counterexamples. Only a candidate that passes is written back.
* Use the {{fill}} hash to classify each spec:
** *in sync*: nothing to do
** *stale*: spec changed, body untouched; regenerate
** *hand-edited*: body changed by a person; never overwrite. Offer to update 
the spec (as a diff for a person to approve, and only if the edited body still 
satisfies the contracts), drop {{@Spec}}, or report the drift
** *conflict*: both changed; a person resolves it
** *unfilled*: no body yet
* Confirmation scales with how well a fill was checked: proved fills apply 
without asking; fills whose contracts or tests pass apply and are shown for 
review; compile-only fills ask; failures leave the hole unfilled with the last 
diagnostics. Updating a spec from code always asks.
* A non-interactive mode for CI fails the build when any spec is stale, 
hand-edited or unfilled.

h2. Open questions

* *Home*: should the annotation and transform start in core 
({{groovy.transform.Spec}}), in groovy-contracts next to 
{{@Requires}}/{{@Ensures}}, or outside the project until the convention has 
proved itself?
* *Retention*: source only, or class retention so method-level specs are 
visible to reflection and to tools reading compiled libraries?
* *Hash format*: what exactly goes into {{fill}}, and should 
whitespace/formatting changes to the body count as a hand edit?
* *Static scope*: should a {{@Spec}} method be compiled statically by default, 
so checkers see every call in generated code? Probes on 6.0.0 show per-method 
static compilation works, including closures and anonymous inner classes in the 
method; a loop-level spec would require compiling the whole enclosing method 
statically.

h2. Out of scope

* Calling a model from the compiler or from an AST transform.
* Runtime (LACUNA-style) holes filled while the program runs; those need a 
typed binding (GROOVY-12462) and a restricted compiler profile (GEP pending), 
and can build on this later.


  was:
_This is a spike of a feature several other languages are experimenting 
with/supporting. It is essentially an annotation that records a comment (not 
unlike @Groovydoc) but can have special meaning for tooling - allowing 
"hole-driven" development._

h2. Summary

Add a {{@Spec}} annotation that marks a method (and optionally a loop or local 
declaration) as having a prose specification intended as an LLM prompt, 
alongside the method's typed signature and any {{@Requires}}/{{@Ensures}} 
contracts. A method carrying {{@Spec}} with an empty body is an unfilled 
"hole". A tool (outside the compiler) fills the hole ahead of time by writing a 
generated body into the source under the spec, and the spec stays in place as 
documentation and as the input for later regeneration.

The annotation is essentially a structured comment: unlike a plain comment, 
tools can find it, and the compiler can tell a filled body from an unfilled one.

h2. Motivation

Martin Odersky's Scala Days 2026 keynote showed program holes filled ahead of 
time: a method with a typed signature, a prose spec string and {{???}}, and a 
"sync" step where an agent writes the body under the spec. Groovy can offer the 
same workflow with a stronger guarantee, because a fill can be checked against 
contracts, not just against types.

* The spec is written for both people and models, and it lives next to the code 
it describes.
* Contracts make the spec checkable. For example, the keynote's own generated 
date parser accepts 31 February; a one-line {{@Ensures}} catches it.
* No runtime evaluation, in-process compilation or typed binding is needed: the 
generated body sits in real source and the compiler sees its real scope.
* Writing contracts for this example already found GROOVY-12463 (contracts 
skipped on a trailing implicit return after an early {{return}}).

h2. Example

{code:groovy}
import groovy.contracts.Ensures
import groovy.contracts.Requires

class Dates {
    @Spec('''
        Parse an ISO-8601 calendar date (yyyy-MM-dd) into its year, month
        and day. Reject dates that do not exist, such as 2026-02-31.
    ''')
    @Requires({ text != null })
    @Ensures({ result.month in 1..12 && result.day <= YearMonth.of(result.year, 
result.month).lengthOfMonth() })
    static DateParts parse(String text) { }   // unfilled: throws until synced
}
{code}

After a sync, the body is filled in and the {{@Spec}} remains, with a {{fill}} 
member recording what was generated (see below).

h2. Proposal

h3. 1. The annotation

* {{@Spec(String value)}}: the prose specification.
* Target: methods; also {{@ExtendedTarget(ExtendedElementType.LOOP)}} and local 
variable declarations for smaller holes. Probes on 6.0.0 confirm the annotation 
can be placed on {{for}}, {{while}} and {{do-while}} loops and on local 
declarations.
* {{fill}} member (optional, written by tooling): a hash of the spec text and 
of the generated body, so a tool can tell whether the spec or the body has 
changed since the last fill.
* Source/class retention to be decided. Loop and local annotations are not 
written to the class file in any case, so those specs exist only in source.

h3. 2. A small local transform

* A method carrying {{@Spec}} with an empty body is compiled to throw (for 
example {{UnsupportedOperationException("unfilled @Spec: Dates.parse")}}) 
instead of returning {{null}}. An unfilled loop can be made to throw similarly.
* The transform never calls a model. Compiling still runs nothing beyond what 
it runs today.

h3. 3. The sync tool (outside core)

Not part of this ticket's compiler change, but it defines the contract the 
annotation must support. A library with a command line (and possibly an MCP 
tool) that IDE plugins and CI call rather than reimplement:

* Find unfilled specs; prompt the model with the prose, contracts, signature 
and types in scope.
* Compile each candidate with the configured type checkers, check contracts (by 
proof via groovy-verify where possible, otherwise by tests), and retry on 
diagnostics or counterexamples. Only a candidate that passes is written back.
* Use the {{fill}} hash to classify each spec:
** *in sync*: nothing to do
** *stale*: spec changed, body untouched; regenerate
** *hand-edited*: body changed by a person; never overwrite. Offer to update 
the spec (as a diff for a person to approve, and only if the edited body still 
satisfies the contracts), drop {{@Spec}}, or report the drift
** *conflict*: both changed; a person resolves it
** *unfilled*: no body yet
* Confirmation scales with how well a fill was checked: proved fills apply 
without asking; fills whose contracts or tests pass apply and are shown for 
review; compile-only fills ask; failures leave the hole unfilled with the last 
diagnostics. Updating a spec from code always asks.
* A non-interactive mode for CI fails the build when any spec is stale, 
hand-edited or unfilled.

h2. Open questions

* *Home*: should the annotation and transform start in core 
({{groovy.transform.Spec}}), in groovy-contracts next to 
{{@Requires}}/{{@Ensures}}, or outside the project until the convention has 
proved itself?
* *Retention*: source only, or class retention so method-level specs are 
visible to reflection and to tools reading compiled libraries?
* *Hash format*: what exactly goes into {{fill}}, and should 
whitespace/formatting changes to the body count as a hand edit?
* *Static scope*: should a {{@Spec}} method be compiled statically by default, 
so checkers see every call in generated code? Probes on 6.0.0 show per-method 
static compilation works, including closures and anonymous inner classes in the 
method; a loop-level spec would require compiling the whole enclosing method 
statically.

h2. Out of scope

* Calling a model from the compiler or from an AST transform.
* Runtime (LACUNA-style) holes filled while the program runs; those need a 
typed binding (GROOVY-12462) and a restricted compiler profile (GEP pending), 
and can build on this later.



> @Spec spike
> -----------
>
>                 Key: GROOVY-12468
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12468
>             Project: Groovy
>          Issue Type: New Feature
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> _This is a spike of a feature several other languages are experimenting 
> with/supporting. It is essentially an annotation that records a comment (not 
> unlike @Groovydoc) but can have special meaning for tooling - allowing 
> "hole-driven" development._
> h2. Summary
> Add a {{@Spec}} annotation that marks a method (and optionally a loop or 
> local declaration) as having a prose specification intended as a human (think 
> pair programming/TDD) or LLM prompt, alongside the method's typed signature 
> and any {{@Requires}}/{{@Ensures}} contracts. A method carrying {{@Spec}} 
> with an empty body is an unfilled "hole". A human or tool (outside the 
> compiler) fills the hole ahead of time by writing a generated body into the 
> source under the spec, and the spec stays in place as documentation and as 
> the input for later regeneration by an agent or validation by a human 
> reviewer.
> The annotation is essentially a structured comment: unlike a plain comment, 
> tools can find it, and the compiler can tell a filled body from an unfilled 
> one.
> h2. Motivation
> Martin Odersky's Scala Days 2026 keynote showed program holes filled ahead of 
> time: a method with a typed signature, a prose spec string and {{???}}, and a 
> "sync" step where an agent writes the body under the spec. Groovy can offer 
> the same workflow with a stronger guarantee, because a fill can be checked 
> against contracts, not just against types.
> * The spec is written for both people and models, and it lives next to the 
> code it describes.
> * Contracts make the spec checkable. For example, the keynote's own generated 
> date parser accepts 31 February; a one-line {{@Ensures}} catches it.
> * No runtime evaluation, in-process compilation or typed binding is needed: 
> the generated body sits in real source and the compiler sees its real scope.
> * Writing contracts for this example already found GROOVY-12463 (contracts 
> skipped on a trailing implicit return after an early {{return}}).
> h2. Example
> {code:groovy}
> import groovy.contracts.Ensures
> import groovy.contracts.Requires
> class Dates {
>     @Spec('''
>         Parse an ISO-8601 calendar date (yyyy-MM-dd) into its year, month
>         and day. Reject dates that do not exist, such as 2026-02-31.
>     ''')
>     @Requires({ text != null })
>     @Ensures({ result.month in 1..12 && result.day <= 
> YearMonth.of(result.year, result.month).lengthOfMonth() })
>     static DateParts parse(String text) { }   // unfilled: throws until synced
> }
> {code}
> After a sync, the body is filled in and the {{@Spec}} remains, with a 
> {{fill}} member recording what was generated (see below).
> h2. Proposal
> h3. 1. The annotation
> * {{@Spec(String value)}}: the prose specification.
> * Target: methods; also {{@ExtendedTarget(ExtendedElementType.LOOP)}} and 
> local variable declarations for smaller holes. Probes on 6.0.0 confirm the 
> annotation can be placed on {{for}}, {{while}} and {{do-while}} loops and on 
> local declarations.
> * {{fill}} member (optional, written by tooling): a hash of the spec text and 
> of the generated body, so a tool can tell whether the spec or the body has 
> changed since the last fill.
> * Source/class retention to be decided. Loop and local annotations are not 
> written to the class file in any case, so those specs exist only in source.
> h3. 2. A small local transform
> * A method carrying {{@Spec}} with an empty body is compiled to throw (for 
> example {{UnsupportedOperationException("unfilled @Spec: Dates.parse")}}) 
> instead of returning {{null}}. An unfilled loop can be made to throw 
> similarly.
> * The transform never calls a model. Compiling still runs nothing beyond what 
> it runs today.
> h3. 3. The sync tool (outside core)
> Not part of this ticket's compiler change, but it defines the contract the 
> annotation must support. A library with a command line (and possibly an MCP 
> tool) that IDE plugins and CI call rather than reimplement:
> * Find unfilled specs; prompt the model with the prose, contracts, signature 
> and types in scope.
> * Compile each candidate with the configured type checkers, check contracts 
> (by proof via groovy-verify where possible, otherwise by tests), and retry on 
> diagnostics or counterexamples. Only a candidate that passes is written back.
> * Use the {{fill}} hash to classify each spec:
> ** *in sync*: nothing to do
> ** *stale*: spec changed, body untouched; regenerate
> ** *hand-edited*: body changed by a person; never overwrite. Offer to update 
> the spec (as a diff for a person to approve, and only if the edited body 
> still satisfies the contracts), drop {{@Spec}}, or report the drift
> ** *conflict*: both changed; a person resolves it
> ** *unfilled*: no body yet
> * Confirmation scales with how well a fill was checked: proved fills apply 
> without asking; fills whose contracts or tests pass apply and are shown for 
> review; compile-only fills ask; failures leave the hole unfilled with the 
> last diagnostics. Updating a spec from code always asks.
> * A non-interactive mode for CI fails the build when any spec is stale, 
> hand-edited or unfilled.
> h2. Open questions
> * *Home*: should the annotation and transform start in core 
> ({{groovy.transform.Spec}}), in groovy-contracts next to 
> {{@Requires}}/{{@Ensures}}, or outside the project until the convention has 
> proved itself?
> * *Retention*: source only, or class retention so method-level specs are 
> visible to reflection and to tools reading compiled libraries?
> * *Hash format*: what exactly goes into {{fill}}, and should 
> whitespace/formatting changes to the body count as a hand edit?
> * *Static scope*: should a {{@Spec}} method be compiled statically by 
> default, so checkers see every call in generated code? Probes on 6.0.0 show 
> per-method static compilation works, including closures and anonymous inner 
> classes in the method; a loop-level spec would require compiling the whole 
> enclosing method statically.
> h2. Out of scope
> * Calling a model from the compiler or from an AST transform.
> * Runtime (LACUNA-style) holes filled while the program runs; those need a 
> typed binding (GROOVY-12462) and a restricted compiler profile (GEP pending), 
> and can build on this later.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to