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

Paul King updated GROOVY-12468:
-------------------------------
    Issue Type: New Feature  (was: Improvement)

> @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 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)

Reply via email to