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
 

Reply via email to