You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/04/29 02:54:18 UTC
(groovy-website) 02/02: GEP-21 revisions for Shape C
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git
commit 7441cb9e75ab4cbe6e1eebdf66336a2810661d3f
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Apr 29 12:54:06 2026 +1000
GEP-21 revisions for Shape C
---
site/src/site/wiki/GEP-21.adoc | 423 +++++++++++++++++++++++++++++++----------
1 file changed, 325 insertions(+), 98 deletions(-)
diff --git a/site/src/site/wiki/GEP-21.adoc b/site/src/site/wiki/GEP-21.adoc
index 8088b3d..a00fa55 100644
--- a/site/src/site/wiki/GEP-21.adoc
+++ b/site/src/site/wiki/GEP-21.adoc
@@ -7,14 +7,20 @@
[horizontal,options="compact"]
*Number*:: GEP-21
*Title*:: Broadening AST transform awareness under joint compilation
-*Version*:: 1
+*Version*:: 2
*Type*:: Feature
*Status*:: Draft
*Leader*:: TBD
*Created*:: 2026-04-28
-*Last modification* :: 2026-04-28
+*Last modification*:: 2026-04-29
****
+NOTE: A working spike for **Shape C** (the split-transform model described
+below) is available at https://github.com/apache/groovy/pull/2500. It
+covers `@Sortable`, `@TupleConstructor`, `@Lazy`, and `@ToString` end-to-end
+through joint compilation, including before/after stub diffs. Findings
+from that spike are folded into this revision.
+
== Abstract
This GEP extends Groovy's AST transform framework so that opt-in
@@ -188,21 +194,33 @@ This proposal inserts a *fourth* step at `CONVERSION`, after
annotation collection / alias expansion and before stub generation:
[start=4]
-. *Stub contribution* — for each class node, every annotation whose
- transform has opted in to joint-compile awareness gets a chance to
- add Java-visible member signatures to the class node. The added
- members carry placeholder bodies and are tagged so the later body
- pass can recognise and complete them.
+. *Stub contribution* — for each class node, the framework gives
+ every joint-compile-aware annotation an early invocation. The
+ invocation may mutate the AST in any way that influences what
+ `JavaStubGenerator` will subsequently render: adding members
+ (interfaces, methods, constructors, fields) with placeholder
+ bodies, flipping modifiers on existing members so the embedded
+ `Verifier` sub-pass produces a different accessor shape,
+ augmenting or trimming property nodes, and so on. Mutations are
+ typically tagged with a metadata key so the authoritative later
+ invocation — the same transform's normal-phase call in Shapes A
+ and B, or the sibling transform's call in Shape C — can recognise
+ the early contribution and complete it (replacing a placeholder
+ body, discarding a stub-only artifact, etc.).
When stub generation runs (step 3 in the original chain, now step 5),
-it sees the contributed members along with everything else and
-includes them in the rendered stub.
+it sees the post-contribution AST and renders the stub from that.
=== Joint-compile-aware transforms
A transform opts into the new behaviour by declaring itself joint-
-compile-aware. Two equivalent shapes are available, chosen by the
-transform author:
+compile-aware. Three alternative shapes have been identified for
+how a transform expresses this opt-in. They are not mutually
+exclusive — the framework could support any one of them, or any
+combination — and the choice between them is a design question this
+GEP leaves open for discussion (see _Open question: which shapes to
+ship?_ below). The shapes are presented here so the discussion has
+concrete options to weigh.
[source,java]
----
@@ -224,60 +242,194 @@ public class TupleConstructorASTTransformation
implements StubContributor { ... }
----
-In Shape A, the transform's own `visit` method is invoked twice per
-target: once at `CONVERSION` with a context flag (or thread-local
-mode) indicating *stub contribution*, and again at the declared
-phase with the flag indicating *body completion*. The transform
-inspects the flag and self-selects what work to do. State carried
+[source,java]
+----
+// Shape C — split-transform model (existing Groovy idiom, validated by spike)
+@GroovyASTTransformationClass({
+ "org.codehaus.groovy.transform.TupleConstructorASTStubber", // phase = CONVERSION
+ "org.codehaus.groovy.transform.TupleConstructorASTTransformation" // phase = CANONICALIZATION
+})
+public @interface TupleConstructor { ... }
+----
+
+In Shape A, the framework looks for the joint-compile-aware flag
+twice — once at `CONVERSION` during joint processing and once at
+the transform's declared phase. The transform's `visit` is invoked
+in both passes. The transform must be idempotent: state carried
between calls lives on the AST nodes (e.g. via `putNodeMetaData`).
+A transform may distinguish the two passes via a context flag if
+the work is genuinely different, but this is not required by the
+framework.
In Shape B, the framework invokes `contributeStubs(...)` at
`CONVERSION` and the existing `visit(...)` at the declared phase as
today. The two methods are independent code paths in the same
class.
-Shape A is mechanically simpler: no new public interface, no
-additional method on existing transforms, less duplication when the
-same logic produces both signatures and bodies. Shape B is cleaner
-when the two passes are substantially different and the author
-prefers two short methods over one branching method. The proposal
-permits both; the choice is a transform-author decision and bears no
-on-the-wire compatibility consequence.
+In Shape C, the author writes two transform classes and lists both
+in `@GroovyASTTransformationClass`. The first class declares
+`phase = CompilePhase.CONVERSION` and runs only during joint
+compilation; the second class declares its normal phase
+(`CANONICALIZATION`, `SEMANTIC_ANALYSIS`, etc.) and is the
+authoritative pass for runtime output. Existing split-transform
+pairs in the codebase (`SealedASTTransformation` /
+`SealedCompletionASTTransformation`,
+`ExternalizeMethodsASTTransformation` /
+`ExternalizeVerifierASTTransformation`) follow the same idiom but
+both at non-CONVERSION phases; Shape C extends that pattern by
+allowing the first piece at `CONVERSION`.
+
+==== Equivalence and trade-offs
+
+All three shapes face the *same* scoping decision at `CONVERSION`
+(thin AST, no transform-cascade output, no static-type-checker
+results), the *same* subset-of-runtime invariant for stub fidelity,
+and the *same* idempotency requirement. They differ only in:
+
+* *Visible API surface.* Shapes A and B add it (a new annotation
+ attribute, a new interface). Shape C adds none — it composes
+ existing public mechanisms.
+* *Boundary visibility.* Shape C makes the early/late split
+ externally obvious because each phase is its own class. Shapes A
+ and B keep the split internal to a single transform class —
+ equally valid, just less immediately legible to a reader.
+* *Code reuse for shared logic.* In all three shapes, when the early
+ pass and the authoritative pass do overlapping work, a static
+ helper called from both pieces handles the duplication cleanly
+ (the spike demonstrates this with `addComparableSurface` shared
+ between `SortableASTStubber` and `SortableASTTransformation`).
+ This is ordinary refactoring, not a shape-specific tax.
+
+Whichever shapes are ultimately supported, the contract observed by
+the rest of the compiler — what runs when, on which target, with
+which information available — is identical, and a transform's
+choice of shape carries no on-the-wire compatibility consequence
+for users of that transform.
+
+NOTE: The "Stubber" naming convention used in the spike is just a
+naming convention. Where the early piece does substantial AST
+surgery (e.g. `LazyASTStubber` flipping a property modifier to
+suppress setter emission), a more neutral name such as
+`*ASTConversionPart` or `*ASTPreStub` may better reflect scope.
+The framework imposes no naming requirement.
+
+==== Open question: which shapes to ship?
+
+The three shapes can co-exist in the framework — adding any one
+does not preclude later adding the others — but committing to all
+three from day one is a larger surface area than may be warranted.
+Two reasonable positions for the GEP discussion:
+
+* *Ship Shape C only* (the spike's choice). It requires no new
+ public API — only the small framework adjustments documented
+ under _Compilation_ — and it composes existing mechanisms
+ (`@GroovyASTTransformationClass` lists, the standard transform-
+ collector and dispatcher). Shapes A and B can be added later if
+ concrete demand emerges or if some transform's needs aren't
+ comfortably expressible as a split.
+* *Ship Shape A or B in addition* if the in-class re-entrant model
+ is judged worth the visible API cost (a new annotation attribute
+ or a new interface), for example because authors of complex
+ transforms find the single-class model significantly easier to
+ reason about.
+
+This GEP does not pre-decide. Reviewers are invited to weigh in on
+which shapes to commit to in the first implementation.
+
+[[spike-results]]
+=== Spike results: before / after stub views
+
+The spike at https://github.com/apache/groovy/pull/2500 implements
+Shape C for four transforms. The table summarises what a Java source
+in the same joint-compilation unit sees via the generated stub
+before and after the transform opts in.
+
+[cols="1,2,2"]
+|===
+|Transform |Before (no stubber piece) |After (Shape C stubber)
+
+|`@Sortable`
+|Class does not declare `Comparable`; no `compareTo` method is
+visible. Java code calling `task.compareTo(other)` fails to compile
+even though the runtime supports it.
+|`implements java.lang.Comparable<Self>` is on the class header;
+`int compareTo(Self other) { return 0; }` is declared. Java code
+compiles and at runtime hits the real comparison body
+(metadata-key handoff at `CANONICALIZATION`).
+
+|`@TupleConstructor`
+|No tuple constructor visible — only the implicit no-arg. Java code
+writing `new Foo("bar")` fails to compile even though
+`defaults = true` would generate the overload at runtime.
+|For `defaults = true`/`defaultsMode = ON`: the full prefix-overload
+chain (`Foo()`, `Foo(p1)`, ..., `Foo(p1..pN)`) is in the stub,
+mirroring what `Verifier` will produce at runtime. For
+`defaults = false`/`defaultsMode = OFF`/`AUTO`: only the maximal
+form, matching the runtime exactly. (Trait-injected and super
+properties remain invisible — see _Tier 3_.)
+
+|`@Lazy`
+|Stub has both `getX()` and `setX(...)` (auto-property accessors).
+Runtime has only `getX()`. Java code calling `holder.setX(v)`
+compiles against the stub but fails at runtime with
+`NoSuchMethodError`.
+|Stub has only `getX()` — the stubber marks the property final at
+`CONVERSION`, which causes `Verifier` to skip setter generation.
+The full transform later replaces the property entirely, so the
+final marker has no effect on runtime.
-The proposal will fix one of the two shapes as the canonical form
-during implementation. Either way, the contract observed by the rest
-of the compiler — what runs when, on which target, with which
-information available — is identical.
+|`@ToString`
+|`toString()` is inherited from `Object` only — the class does not
+*declare* it. Java code chaining `super.toString()` resolves to
+`Object.toString()` at the source level, even though the runtime
+class has its own override. Tooling that distinguishes declared
+from inherited methods sees no override.
+|`toString()` is declared on the class with a placeholder body in
+the stub. Java's `super.toString()` chains correctly through
+`Object`-virtual dispatch to the runtime body, and tooling that
+introspects declared methods sees the override.
+|===
+
+In all four cases the runtime output is unchanged — these are stub-
+fidelity fixes, not behavioural changes.
=== What a stub contribution may emit
-A stub-contribution call may add to the target class node:
+A stub-contribution call may mutate the target class node in any
+of these ways:
-* New constructors, with parameters, exceptions, generics, and
+* Add new constructors, with parameters, exceptions, generics, and
parameter annotations populated. The body is a placeholder
(`super(...)` if a super constructor must be invoked, otherwise an
empty block).
-* New methods, with the same signature fidelity as constructors. The
- body is a placeholder consistent with the declared return type
+* Add new methods, with the same signature fidelity as constructors.
+ The body is a placeholder consistent with the declared return type
(`return null;`, `return 0;`, `return false;`, or empty for
`void`). The stub printer already understands such placeholders.
-* New fields, with declared type, modifiers, and any constant
+* Add new fields, with declared type, modifiers, and any constant
initializer that is a literal expression. Non-literal initializers
- are deferred to the body pass and rendered as default values in
- the stub.
-* New declared interfaces (`addInterface(...)`).
-* A new declared super class (`setSuperClass(...)`), with the usual
- caveat that changing the super class is a global decision and may
- conflict with other transforms.
-* Annotations on any of the above, including the contributing
+ are deferred to the authoritative pass and rendered as default
+ values in the stub.
+* Add new declared interfaces (`addInterface(...)`).
+* Add a new declared super class (`setSuperClass(...)`), with the
+ usual caveat that changing the super class is a global decision
+ and may conflict with other transforms.
+* Add annotations on any of the above, including the contributing
transform's own marker annotations.
+* Flip modifiers on existing user-declared members where the change
+ has no runtime effect — for example marking a `PropertyNode` final
+ to suppress the `Verifier` sub-pass's setter generation when the
+ authoritative pass will replace the property entirely (the
+ spike's `@Lazy` case).
A stub contribution may *not*:
-* Modify existing user-declared members. Bodies of user-written
- methods, the type of declared fields, and existing constructors
- are owned by the source author and the body pass; the stub pass is
- read-only for those.
+* Change runtime semantics of user-declared members — bodies of
+ user-written methods, types of declared fields, parameter lists of
+ user-written constructors, and similar. These are owned by the
+ source author and the authoritative pass; the stub pass must
+ produce a class node whose runtime is unchanged compared to a
+ pure-Groovy compilation of the same source.
* Add behaviour that depends on inferred types from later phases
(e.g. types derived by `StaticTypeCheckingVisitor`). The transform
must declare types from information available at `CONVERSION`.
@@ -286,32 +438,38 @@ A stub contribution may *not*:
declared members visible at `CONVERSION`; their own transforms
haven't run yet. (See _Excluded and deferred features_.)
-=== Skeleton bodies and the body pass
+=== Skeleton members and the authoritative pass
Every member added by a stub contribution is tagged with a metadata
-flag identifying it as a skeleton awaiting body completion. The
-body pass — the same transform running at its declared phase — uses
-the flag to decide between two paths per member:
-
-* If a matching skeleton already exists on the class node, the body
- pass *fills in the body* of that node (and any further annotations
- or modifiers it intends to add) rather than creating a new node.
- The skeleton flag is cleared.
-* If no matching skeleton exists, the body pass creates the member
- as it does today.
-
-This contract makes the body pass robust against the stub pass
-having been run, having been partially run, or not having been run
-at all (e.g. for a non-joint-compilation build that goes straight
-through `CONVERSION` without touching the stub op chain). A
-transform written for the joint-compile-aware contract continues to
-work in all build modes.
+flag identifying it as a skeleton awaiting completion. The
+authoritative pass — whether that's the same transform running at
+its declared phase (Shapes A and B), or the sibling transform at
+its declared phase (Shape C) — uses the flag to decide between
+paths per member:
+
+* If a matching skeleton already exists on the class node, the
+ authoritative pass *fills in the body* of that node (and any
+ further annotations or modifiers it intends to add) rather than
+ creating a new node. The skeleton flag is cleared.
+* If no matching skeleton exists, the authoritative pass creates
+ the member as it does today.
+* If the authoritative pass supersedes the skeleton entirely (as in
+ the spike's `@TupleConstructor`, where the full transform discards
+ any stubber-tagged constructors before generating its own), the
+ pass removes the skeletons before its main work.
+
+This contract makes the authoritative pass robust against the stub
+pass having been run, having been partially run, or not having been
+run at all (e.g. for a non-joint-compilation build that goes
+straight through `CONVERSION` without touching the stub op chain).
+A transform written for the joint-compile-aware contract continues
+to work in all build modes.
The matching predicate is "same kind, same name, structurally equal
parameter list, same declaring class". Generic substitution is
applied before comparison so that a skeleton declared with type
-variables and a body-pass member declared with the same variables
-match.
+variables and a member emitted by the authoritative pass with the
+same variables match.
=== Ordering of contributions
@@ -389,23 +547,57 @@ mechanism instead.
`JavaAwareCompilationUnit` and `JavaStubCompilationUnit` register a
new phase operation at `Phases.CONVERSION`, ordered between the
existing transform-collector op and the existing stub-generation op.
-The new op walks each top-level class node in the source unit,
-finds every annotation whose registered transform is joint-
-compile-aware, and dispatches the stub-contribution call for each.
+The new op invokes any AST transformations registered for the
+`CONVERSION` phase on each class node.
-The ordering rule is, per source unit, per class:
+The ordering rule for joint compilation is, per source unit, per class:
. `JavaAwareResolveVisitor`
. `ASTTransformationCollectorCodeVisitor` (collects + expands aliases)
-. *new:* `StubContributionVisitor` (calls contributions, in priority order)
+. *new:* `CONVERSION`-phase transform invocation
. `JavaStubGenerator.generateClass(...)`
+The spike implements step 3 by reusing the existing
+`ASTTransformationVisitor` machinery — a public factory
+`ASTTransformationVisitor.invocationOperation(phase, context)`
+returns an `IPrimaryClassNodeOperation` that walks the class with
+the same dispatch logic as the SEMANTIC_ANALYSIS+ phases. No new
+visitor class is required.
+
For non-joint-compilation builds (`CompilationUnit` rather than
-`JavaAwareCompilationUnit`), step 4 is absent. Step 3 is therefore
-either harmless (skeletons get filled in later by the body pass and
-the result is bit-for-bit identical to the no-skeleton path) or, as
-a perf optimisation, also skipped — the body pass handles
-everything as it does today.
+`JavaAwareCompilationUnit`), step 3 is absent. Shape A and Shape B
+transforms simply do their work at the declared phase as today.
+Shape C transforms see only the second (authoritative) piece run;
+the `CONVERSION`-phase piece is silently inert because no invoker
+fires it. This is by design: each shape's later piece must remain
+authoritative for pure-Groovy compilation regardless of how the
+early piece is wired.
+
+==== Required framework adjustments for Shape C
+
+The spike validated two small framework changes:
+
+* The existing guard in
+ `ASTTransformationCollectorCodeVisitor.verifyAndAddTransform`
+ rejected any local transform with `phase < SEMANTIC_ANALYSIS`. It
+ is relaxed to `phase < CONVERSION`. `CONVERSION`-phase local
+ transforms are now legal; they fire only in joint compilation
+ (where step 3 above invokes them) and are inert in pure-Groovy
+ compilation (no invoker exists for `CONVERSION` in standard
+ `CompilationUnit`).
+* `ASTTransformationCustomizer.findASTTransformationClass` previously
+ rejected annotations whose `@GroovyASTTransformationClass` listed
+ more than one transform class. With Shape C, that list grows to
+ two for any opted-in annotation. The customizer now drops
+ `CONVERSION`-phase entries when at least one non-`CONVERSION`
+ sibling exists, and otherwise keeps them all (preserving the
+ existing customizer-only `CONVERSION` use cases such as
+ `ContractAnnotation`).
+
+Other framework code that consumes
+`getAnnotation(GroovyASTTransformationClass).classes()` /
+`.value()` directly should adopt the same filter where the
+intent is "the canonical class for this annotation."
=== Resolver state at CONVERSION
@@ -450,22 +642,25 @@ generic specialisations are available. For a same-unit Groovy
supertype, only source-declared members are available; transform-
added abstract methods on the supertype are not yet seen.
-=== Body-pass replacement contract
+=== Authoritative-pass replacement contract
-The body pass — the existing transform `visit` running at the
-declared phase — is updated for each joint-compile-aware transform
-to:
+The authoritative pass — the same transform's normal-phase `visit`
+in Shapes A and B, or the sibling transform's `visit` in Shape C —
+is updated for each joint-compile-aware transform to:
. Locate any skeleton members on the class node (via the metadata
- flag) that match what the transform would have added.
-. For each match, replace the placeholder body and clear the flag.
-. For non-matches (skeleton pass not run, or new members the
+ flag) that match what the pass would have added.
+. For each match, either replace the placeholder body and clear the
+ flag, or — when the pass produces output that supersedes the
+ skeleton entirely — discard the skeleton before doing its work
+ (the spike's `@TupleConstructor` takes this approach).
+. For non-matches (stub pass not run, or new members the
contribution did not anticipate), behave as today.
-This update is local to each transform. The body pass remains the
-canonical specification of what the transform produces; the stub
-contribution is constrained to be a subset (signature-only,
-annotations) of the body pass.
+This update is local to each transform. The authoritative pass
+remains the canonical specification of what the transform produces;
+the stub contribution is constrained to be a subset (signature-only
+plus modifier nudges, no body content) of the authoritative pass.
== Migration
@@ -478,22 +673,29 @@ These transforms emit member signatures that depend only on the
declared class generics and the declared field types. All required
information is unconditionally available at `CONVERSION`.
-[cols="2,3"]
+[cols="2,3,1"]
|===
-|Transform |Stub contribution
+|Transform |Stub contribution |Status
|`@TupleConstructor`
|Constructor signatures, including the cascade produced by the
- `defaults = true` shape. Parameters use field types with class
- generics applied. `pre` / `post` closures are body-only.
+ `defaults = true` shape. Parameters use directly-declared property
+ types with class generics applied. Trait-injected and super
+ properties are not visible at `CONVERSION` (see _Tier 3_); the
+ stubber's view is therefore a strict subset of the runtime
+ surface for those configurations. `pre` / `post` closures are
+ body-only.
+|*Spike: validated*
|`@MapConstructor`
|`MyClass(Map)` constructor signature.
+|Pending
|`@Singleton`
|Public static `INSTANCE` field of the enclosing type, public static
`getInstance()` returning the enclosing type. Both with the
enclosing class's generics applied.
+|Pending
|`@Bindable` (class-level)
|`addPropertyChangeListener(PropertyChangeListener)`,
@@ -503,25 +705,33 @@ information is unconditionally available at `CONVERSION`.
`getPropertyChangeListeners(String)`. Plus the
`propertyChangeSupport` field if non-private propagation is
desired.
+|Pending
|`@Vetoable` (class-level)
|Symmetric to `@Bindable` with `VetoableChangeListener`.
+|Pending
|`@ListenerList`
|`addXxxListener`, `removeXxxListener`, `getXxxListeners`, and a
`fireXxx(...)` per listener-interface method. Listener interface
method enumeration uses the field type's resolved methods.
+|Pending
|`@ExternalizeMethods`
|`writeExternal(ObjectOutput)`, `readExternal(ObjectInput)`, plus
`Externalizable` on the implements clause.
+|Pending
|`@IndexedProperty`
|Per indexed field: `getXxx(int)`, `setXxx(int, T)`, with `T` from
the field's element type.
+|Pending
|`@Sortable`
-|`compareTo(T)` and `Comparable<T>` on the implements clause.
+|`compareTo(T)` placeholder and `Comparable<T>` on the implements
+ clause; full transform replaces the placeholder body via the
+ metadata-key handoff at `CANONICALIZATION`.
+|*Spike: validated*
|===
These transforms are the proposal's primary beneficiaries. A Java
@@ -537,34 +747,47 @@ with Tier 1 transforms, or emit members whose Java visibility is
already adequately covered by inheritance. They migrate after Tier
1 with modest additional plumbing.
-[cols="2,3"]
+[cols="2,3,1"]
|===
-|Transform |Notes
+|Transform |Notes |Status
|`@ToString`
-|Emits `toString()`. Java callers reach it via `Object`-inherited
- dispatch even without a stub contribution, but the contribution
- makes the stub faithful (and unblocks `MethodHandles.lookup` /
- reflection-style code that introspects the stub).
+|Emits a placeholder `toString()` at `CONVERSION`; the full transform
+ replaces the body via the metadata-key handoff. Java callers reach
+ `toString()` via `Object`-inherited dispatch either way, but the
+ contribution makes the stub faithful — `super.toString()` chains
+ from a Java subclass resolve to the overridden form at runtime,
+ and tooling that distinguishes declared from inherited methods
+ sees the override.
+|*Spike: validated*
|`@EqualsAndHashCode`
|Same shape as `@ToString` — already callable via `Object` but a
contribution improves stub fidelity.
+|Pending
|`@Lazy`
-|Verifier already synthesises an accessor for the property; the
- contribution refines the field shape.
+|`Verifier` synthesises both `getX()` and `setX(...)` for the
+ property by default. The stubber marks the property final at
+ `CONVERSION`, which causes `Verifier` to skip setter generation
+ (a single modifier flip — no need to duplicate the full transform's
+ backing-field rewiring). The full transform replaces the property
+ entirely at `SEMANTIC_ANALYSIS`, so the final marker has no runtime
+ effect.
+|*Spike: validated*
|`@Builder` (`SimpleStrategy`, `DefaultStrategy`)
|Inner `Builder` class with fluent setters and a `build()` method.
Configuration is in the `@Builder` annotation values
(`builderClassName`, `builderMethodName`, `prefix`, etc.) and is
fully readable at `CONVERSION`.
+|Pending
|`@AutoClone`
|Emits `clone()` and adds `Cloneable` to the implements clause. The
interface addition needs the contribution; `clone()` itself is
already on `Object`.
+|Pending
|`@InheritConstructors`
|Mirrors super-class constructors. Works for classpath super
@@ -572,11 +795,13 @@ already adequately covered by inheritance. They migrate after Tier
`@TupleConstructor`-family transforms, the super's transform-added
constructors are not yet visible at `CONVERSION` (deferred — see
below).
+|Pending
|`@RecordType` / record-style classes
|Component accessors and the canonical constructor are derivable
from declared components. Some record-specific finalisation runs
- later and is left to the body pass.
+ later and is left to the authoritative pass.
+|Pending
|===
=== Tier 3 — deferred
@@ -728,9 +953,11 @@ already adequately covered by inheritance. They migrate after Tier
== References
+* https://github.com/apache/groovy/pull/2500[GEP-21 spike PR — Shape C implementation for `@Sortable`, `@TupleConstructor`, `@Lazy`, and `@ToString`]
* https://groovy-lang.org/metaprogramming.html#_compilation_phases_guide[Groovy compilation phases guide]
* https://github.com/apache/groovy/blob/master/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java[`JavaStubGenerator` — current stub generator, including the `Verifier` sub-pass and the `@PackageScope` / trait back-channels]
* https://github.com/apache/groovy/blob/master/src/main/java/org/codehaus/groovy/tools/javac/JavaAwareCompilationUnit.java[`JavaAwareCompilationUnit` — joint-compilation phase op chain]
* https://github.com/apache/groovy/blob/master/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java[`ASTTransformationCollectorCodeVisitor` — `@AnnotationCollector` alias expansion at CONVERSION]
* https://github.com/apache/groovy/blob/master/src/main/java/org/codehaus/groovy/transform/ASTTransformationVisitor.java[`ASTTransformationVisitor` — phase-dispatch wiring for local transforms]
* https://groovy-lang.org/objectorientation.html#_traits[Traits — example of an existing stub-side back-channel for non-injection rendering decisions]
+* Existing split-transform precedents in the codebase: `SealedASTTransformation` / `SealedCompletionASTTransformation`, `ExternalizeMethodsASTTransformation` / `ExternalizeVerifierASTTransformation`. Both pairs run after `CONVERSION`; Shape C extends the same idiom by allowing the first piece at `CONVERSION`.