[
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 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.
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, and can build
on this later.
> @Spec spike
> -----------
>
> Key: GROOVY-12468
> URL: https://issues.apache.org/jira/browse/GROOVY-12468
> Project: Groovy
> Issue Type: Bug
> 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 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.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)