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/07/11 22:09:56 UTC

(groovy-website) branch asf-site updated: GEP-27: update to reflect latest GROOVY-12151 slice

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 <pa...@asert.com.au>
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