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
The following commit(s) were added to refs/heads/asf-site by this push:
new 974f01c GEP-27: update to reflect latest GROOVY-12151 slice
974f01c is described below
commit 974f01cbe56955a33337803efd99da28e61b8148
Author: Paul King <[email protected]>
AuthorDate: Sun Jul 12 08:09:45 2026 +1000
GEP-27: update to reflect latest GROOVY-12151 slice
---
site/src/site/wiki/GEP-27.adoc | 134 +++++++++++++++++++++++++----------------
1 file changed, 81 insertions(+), 53 deletions(-)
diff --git a/site/src/site/wiki/GEP-27.adoc b/site/src/site/wiki/GEP-27.adoc
index a342236..1131400 100644
--- a/site/src/site/wiki/GEP-27.adoc
+++ b/site/src/site/wiki/GEP-27.adoc
@@ -10,19 +10,20 @@
*Version*:: 1
*Type*:: Feature
*Status*:: Draft
-*Target*:: Groovy 6.0 (potential first step) and Groovy 7.0 (remainder)
-*Comment*:: Reduces the per-closure/per-lambda generated-class explosion by
hoisting bodies onto the enclosing class. A small, sound-by-construction slice
potentially targets Groovy 6; the broader closure work targets Groovy 7.
Strictly additive; gated by a flag for back-out.
+*Target*:: Groovy 6.0 (SAM lambdas merged; closure packing a stretch goal) and
Groovy 7.0 (object elision, writer unification)
+*Comment*:: Reduces the per-closure/per-lambda generated-class explosion by
hoisting bodies onto the enclosing class. The sound-by-construction SAM-lambda
slice is merged for Groovy 6; closure packing (S1) is implemented and proposed
as a Groovy 6 stretch goal (GROOVY-12151); object elision and writer
unification remain for Groovy 7. Strictly additive; gated by a flag for
back-out.
*Leader*:: Paul King
*Created*:: 2026-07-09
-*Last modification*:: 2026-07-11
+*Last modification*:: 2026-07-12
****
[width="80%",align="center"]
|===
a| NOTE: _WARNING:_
Material on this page is still under development!
-We are currently working on Groovy 6.0. A small, self-contained part of this
proposal targets Groovy 6.0;
-the larger part targets Groovy 7.0.
+We are currently working on Groovy 6.0. The sound-by-construction SAM-lambda
part of this proposal is
+merged for Groovy 6.0, closure packing is implemented and proposed as a Groovy
6.0 stretch goal, and
+the remainder (object elision, writer unification) targets Groovy 7.0.
The final version of this proposal may differ significantly from the current
draft,
but having this draft available allows us to gather early feedback, align
design
decisions in Groovy 6 as best we can, and iterate on the design.
@@ -52,8 +53,10 @@ actually needs:
* where a value neither escapes nor needs closure identity, the object can be
elided entirely.
The proposal is deliberately phased. A **small, sound-by-construction slice —
eliminating the
-generated class for non-capturing and read-only-capturing SAM lambdas —
targets Groovy 6.0**. The
-**broader closure work, which needs a soundness analysis and a performance
path, targets Groovy 7.0**. The change is
+generated class for non-capturing and read-only-capturing SAM lambdas — is
merged for Groovy 6.0**.
+**Closure packing (S1), including its soundness analysis and a typed-dispatch
performance path, is
+now implemented and proposed as a Groovy 6.0 stretch goal (GROOVY-12151)**;
the remaining, more
+aggressive work — object elision and unifying the two writers — targets Groovy
7.0. The change is
strictly additive, defaults can be flipped via a flag for back-out, and it is
gated so that any
tooling that reflects on generated-class names has an escape hatch.
@@ -336,7 +339,7 @@ modes:
|`PackedClosure` throws on `setDelegate`/`setResolveStrategy` |runtime |the
_invisible_ wrong assumption — "did anyone misuse what I packed?"
|===
-A conservative *escape gate* shrinks the runtime surface further and is
implemented in the spike: a
+A conservative *escape gate* shrinks the runtime surface further and is
implemented in the S1 slice: a
closure literal that visibly escapes its method — stored into a
field/property/index (the Grails
`static constraints = { … }` shape), returned, appended with `<<`, or placed
in a collection literal
— is declined at compile time and kept as a class. This moves the clearest
delegate-risk cases from
@@ -347,10 +350,14 @@ fully self-contained — every free name binds to the owner
or its own parameter
delegate-independent and safe to pack even where it escapes; exploiting that
for nested closures
needs owner-correct retargeting, see _Reference implementation_.)
-== Groovy 6.0 deliverables (merged)
+== Groovy 6.0 deliverables
-The Groovy 6 target is the sound-by-construction slice: eliminating the
generated class for SAM
-lambdas. Both pieces below are merged (GROOVY-12143), behind the opt-in
`groovy.target.lambda.hoist`
+Groovy 6 lands in two parts: the *merged* sound-by-construction SAM-lambda
slice (pieces 1–2), and
+closure packing (S1), *implemented and proposed as a stretch goal*
(GROOVY-12151).
+
+=== Merged: SAM-lambda class elimination
+
+Both pieces below are merged (GROOVY-12143), behind the opt-in
`groovy.target.lambda.hoist`
flag; the mutated-capture and serializable cases correctly stay on the
class-based path.
=== Piece 1 (merged): eliminate the class for non-capturing SAM lambdas
@@ -395,33 +402,45 @@ typed leading parameters. A lambda that *mutates* a
capture still needs the shar
it correctly declines and keeps its class. This lands the bulk of the
capturing benefit in Groovy 6
behind the same flag; only mutated-capture lambdas remain on the class-based
path.
+=== Stretch goal (implemented): closure packing (S1)
+
+A shared `Closure` adapter (`PackedClosure`) that dispatches to a hoisted
body, applied to
+closures rather than lambdas — now implemented in the code generator
(GROOVY-12151) and proposed
+for Groovy 6.0 as a stretch goal. It covers the hard cases: no-capture and
captured closures (by
+value; written captures via shared `Reference`, including `+=`/`++`),
*arbitrary-depth nested
+closures*, implicit `it`, under both `@CompileStatic` and dynamic compilation,
correctly declining
+closures that use `delegate`/`owner`/`super`. All three safety mechanisms from
_Guarding the
+dynamic trust assertion_ are implemented and validated: the mandatory
`PackedClosure` runtime
+guard, the compile-time escape decline, and the `@PackedClosures(mode = WARN |
STRICT)` decline
+diagnostics. The two headline capabilities are done:
+
+* *Capability/escape analysis* — packing is provably sound and therefore
*automatic* under
+ `@CompileStatic` (no annotation) via the type checker's
delegate-independence proof, falling back
+ to S0 (a normal closure class) otherwise; under dynamic compilation it is
opt-in via
+ `@PackedClosures`, backed by the runtime guard.
+* *Typed static dispatch* — the hoisted body is typed from the inferred types
the type checker
+ already computed and compiles statically (typed captures/params/return, zero
`invokedynamic` in
+ the body), so packing under `@CompileStatic` is a speed win and not merely a
class-count win.
+
+The API surface (`@PackedClosures`, `PackMode`) is marked `@Incubating`. What
keeps this a stretch
+goal rather than a committed 6.0 deliverable is that it is opt-in only (the
automatic path is not
+yet default-on) and the nested-closure, elision, and writer-unification work
below is still open —
+all Groovy 7.0.
+
== Groovy 7.0 deliverables
-The broader, higher-value work — where the soundness analysis and the
performance story live.
+The remaining, higher-value work, plus the hardening that turns closure
packing from an opt-in
+stretch goal into a default-on feature.
-=== Closure packing (S1) with a soundness analysis
+=== Owner-correct retargeting for nested closures
-A shared `Closure` adapter (`PackedClosure`) that dispatches to a hoisted
body, applied to
-closures rather than lambdas. A proof-of-concept in the code generator already
demonstrates the
-mechanism across the hard cases: no-capture and captured closures (by value;
written captures via
-shared `Reference`, including `+=`/`++`), *arbitrary-depth nested closures*,
implicit `it`, under
-both `@CompileStatic` and dynamic compilation, correctly declining closures
that use
-`delegate`/`owner`/`super`. The dynamic opt-in's two runtime/compile safety
mechanisms from
-_Guarding the dynamic trust assertion_ are already implemented and validated
in the spike: the
-mandatory `PackedClosure` runtime guard, the compile-time escape decline, and
the
-`@PackedClosures(mode = WARN | STRICT)` decline diagnostics. What remains for
a real feature
-is the *capability/escape analysis* that makes packing provably sound and
therefore automatic under
-`@CompileStatic` (no annotation), falling back to S0 otherwise; and the
owner-correct retargeting
-that lets self-contained nested closures (e.g. Grails `validator` blocks) be
hoisted — see
-_Reference implementation_.
-
-=== Typed static dispatch (making it fast, not just small)
-
-In the proof-of-concept the hoisted body is compiled dynamically, so a packed
`@CompileStatic`
-closure is smaller but dispatched dynamically. A production form must type the
hoisted method
-from the inferred types the type checker already computed and dispatch the
body statically (with
-an `invokedynamic`/`MethodHandle` callsite), so that packing under
`@CompileStatic` is a speed
-win and not merely a class-count win.
+Today a self-contained *nested* closure (e.g. a Grails `validator: { … }`
block inside
+`constraints`) is kept as a class, because hoisting it naively adds the body
to the *enclosing
+closure* class with the wrong owner (see _Grails artefact DSLs and nested
closures_). Hoisting it
+soundly requires retargeting the method onto the correct outermost owner,
rebinding its free names
+to that owner, and emitting static-vs-instance dispatch to match. This is the
single highest-value
+target — domain classes generate the most closure classes — and the reason it
is a separate, later
+phase rather than a cheap gate change.
=== Object elision (S3)
@@ -434,6 +453,13 @@ proof and, for iteration intrinsics, knowledge of the
callee.
Fold the lambda and closure code paths behind the single hoist backend, so the
SAM and
`Closure` cases are one mechanism selecting different wrappers.
+=== Default-on hardening
+
+Flip closure packing from experimental (opt-in `groovy.target.closure.pack`)
to default-on. This is
+a policy decision gated on the remaining human-only tooling verification (see
_IDE and tooling
+considerations_) and on migrating the tests that assert the old bytecode shape
— not on further
+mechanism work.
+
== Configuration and flags
Each landing is gated so it can be disabled without recompiling application
code, following
@@ -446,7 +472,7 @@ means byte-for-byte today's behaviour.
|Concern |Flag |Default
|SAM lambda hoisting (Groovy 6, merged) |`groovy.target.lambda.hoist` |off
(opt-in)
-|Closure packing under `@CompileStatic` (Groovy 7)
|`groovy.target.closure.pack` (+ per-scope `@PackedClosures`) |to be decided
per release
+|Closure packing under `@CompileStatic` (Groovy 6 stretch, GROOVY-12151)
|`groovy.target.closure.pack` (+ per-scope `@PackedClosures`) |off (opt-in);
default-on is Groovy 7
|Closure packing under dynamic compilation |`@PackedClosures` opt-in only |off
unless annotated
|===
@@ -540,8 +566,9 @@ build, so they remain open items before the flag defaults
on:
== Reference implementation
-A proof-of-concept exists as a spike in the code generator, exercised through
this proposal's
-development:
+Both legs are implemented in the code generator and exercised through this
proposal's development —
+the SAM-lambda leg merged, the closure-packing leg on a PR (GROOVY-12151)
proposed as a Groovy 6
+stretch goal:
* *Lambda first step (Groovy 6, merged).* Implemented in
`StaticTypesLambdaWriter`: both
non-capturing *and* read-only-capturing SAM lambda classes are eliminated,
with the impl hoisted
@@ -554,8 +581,8 @@ development:
first step is merged behind the opt-in `groovy.target.lambda.hoist` flag;
the lambda tests that
assert the old bytecode shape are skipped under the flag, with their
migration to the new
structure deferred to when the default flips.
-* *Closure packing (Groovy 7 direction).* A `ClosureWriter`-based spike (with
a shared
- `PackedClosure` runtime adapter), opt-in via `@PackedClosures`, demonstrates
hoisting closures — captures,
+* *Closure packing (Groovy 6 stretch goal, GROOVY-12151).* A
`ClosureWriter`-based implementation
+ (with a shared `PackedClosure` runtime adapter), opt-in via
`@PackedClosures`, hoists closures — captures,
arbitrary-depth nesting, written captures via `Reference`, implicit `it` —
under both compilation
modes. It confirms the writer is the right home (no `@CompileStatic`
boundary, unlike an
AST-transform approach), establishes the soundness cliff empirically, and
confirms what is
@@ -577,10 +604,10 @@ development:
Also implemented and validated: the *capability analysis* (STC's
delegate-independence proof driving
automatic packing under `@CompileStatic`, no annotation needed) and *typed
static dispatch*
(statically-compiled hoisted bodies with typed captures/params/return — zero
`invokedynamic`), with
-compile-time packing exercised across the core module and several others. The
remaining Groovy 7
-work is S3 object elision, unifying the closure and lambda writers,
owner-correct retargeting so
-nested closures can hoist, and hardening the automatic path from experimental
(opt-in flag) to
-default-on.
+compile-time packing exercised across the core module and several others. This
implemented slice is
+proposed as a Groovy 6.0 stretch goal (GROOVY-12151). The remaining Groovy 7
work is owner-correct
+retargeting so self-contained nested closures can hoist, S3 object elision,
unifying the closure and
+lambda writers, and hardening the automatic path from experimental (opt-in
flag) to default-on.
=== Measured impact
@@ -627,15 +654,15 @@ suite before it lands.
|===
|Phase |What ships |What works |Target
-|1 |Non-capturing SAM lambda class elimination + flag + test migration +
IDE/tooling check |Java-parity lambda codegen for the common SAM case; back-out
flag |Groovy 6.0
-|2 |Read-only-capturing SAM lambdas (metafactory-captured static impl)
|Removes the class, `Closure` alloc, and `Reference` wrapping for read-only
capturing SAM lambdas (mutated captures decline) |Groovy 6.0
-|3 |Closure packing (S1) + capability/escape analysis + `@PackedClosures`
|Sound closure packing: automatic under `@CompileStatic`, opt-in (with runtime
guard + escape decline) under dynamic |Groovy 7.0
-|4 |Typed static dispatch of hoisted bodies |Packed `@CompileStatic` closures
are faster, not just smaller |Groovy 7.0
-|5 |Object elision (S3) + unify the writers |No object for non-escaping
call-once bodies; one backend for closures and lambdas |Groovy 7.0
+|1 |Non-capturing SAM lambda class elimination + flag + test migration +
IDE/tooling check |Java-parity lambda codegen for the common SAM case; back-out
flag |Groovy 6.0 (merged)
+|2 |Read-only-capturing SAM lambdas (metafactory-captured static impl)
|Removes the class, `Closure` alloc, and `Reference` wrapping for read-only
capturing SAM lambdas (mutated captures decline) |Groovy 6.0 (merged)
+|3 |Closure packing (S1) + capability/escape analysis + typed static dispatch
+ `@PackedClosures` |Sound closure packing: automatic and statically dispatched
under `@CompileStatic`, opt-in (with runtime guard + escape decline) under
dynamic |Groovy 6.0 (stretch, GROOVY-12151)
+|4 |Owner-correct retargeting so self-contained nested closures hoist |Grails
domain `validator` blocks and other nested closures pack |Groovy 7.0
+|5 |Object elision (S3) + unify the writers + default-on hardening |No object
for non-escaping call-once bodies; one backend for closures and lambdas;
packing default-on |Groovy 7.0
|===
-Phase 1 stands alone and delivers value with no dependency on the later
phases. Phases 3–5 build
-on the shared backend proven in phases 1–2.
+Phases 1–2 stand alone and deliver value with no dependency on the later
phases. Phase 3 (the
+stretch goal) builds on the shared backend proven in phases 1–2; phases 4–5
build on phase 3.
== Excluded and deferred features
@@ -676,9 +703,10 @@ the compatibility valve while tools are updated.
* Fewer generated classes: less classloading, verification, JIT profiling, and
metaspace.
* Non-capturing SAM lambdas keep their zero-allocation metafactory singleton
(the class removed
is dead weight, not an allocation).
-* Closure packing under `@CompileStatic` is a class-count win immediately and
a dispatch-speed
- win once typed dispatch (phase 4) lands; without it, a packed
`@CompileStatic` closure trades a
- class for dynamic dispatch, so typed dispatch is a prerequisite for turning
it on for hot code.
+* Closure packing under `@CompileStatic` is both a class-count win and a speed
win: the hoisted
+ body is typed from the checker's inferred types and compiles statically
(zero `invokedynamic` in
+ the body), implemented in the S1 slice — so a packed `@CompileStatic`
closure is smaller without
+ trading its body's static compilation for dynamic dispatch.
=== Security