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 436de33 updated with recent changes
436de33 is described below
commit 436de333a2164b5e3768bd37c9417e27baf7b2ed
Author: Paul King <[email protected]>
AuthorDate: Sun Jul 12 23:44:12 2026 +1000
updated with recent changes
---
site/src/site/wiki/GEP-27.adoc | 49 ++++++++++++++++++++++++++++++------------
1 file changed, 35 insertions(+), 14 deletions(-)
diff --git a/site/src/site/wiki/GEP-27.adoc b/site/src/site/wiki/GEP-27.adoc
index 1131400..b95ab65 100644
--- a/site/src/site/wiki/GEP-27.adoc
+++ b/site/src/site/wiki/GEP-27.adoc
@@ -54,8 +54,9 @@ actually needs:
The proposal is deliberately phased. A **small, sound-by-construction slice —
eliminating the
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
+**Closure packing (S1), including its soundness analysis and a
`MethodHandle`-based dispatch path that
+reaches `@CompileStatic` call-parity, 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.
@@ -418,9 +419,14 @@ diagnostics. The two headline capabilities are done:
`@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.
+* *Fast dispatch* — two parts. 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). And the shared adapter reaches that body through a `MethodHandle`
constant to the private
+ hoisted method, invoked with `invokeExact` (no reflection), behind a
`doCall` fast path that skips
+ argument adaptation on the common exact-arity call. Measured per `call()`
against the generated
+ closure class it replaces: a naive shared adapter is ~7–9× slower, whereas
the shipped one reaches
+ `@CompileStatic` parity and ~2× under dynamic compilation — so packing is a
class-count win at no
+ dispatch-speed penalty under `@CompileStatic`.
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
@@ -602,10 +608,14 @@ stretch goal:
a scope out entirely. The automatic flag path stays lenient.
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. This
implemented slice is
-proposed as a Groovy 6.0 stretch goal (GROOVY-12151). The remaining Groovy 7
work is owner-correct
+automatic packing under `@CompileStatic`, no annotation needed) and the
*dispatch path* — a
+statically-compiled, typed hoisted body reached through a `MethodHandle`
constant to the private
+hoisted method (`invokeExact`, no reflection) behind a lean `doCall` fast path
— exercised across the
+core module and several others. Measured per `call()` against the generated
closure class it replaces,
+a naive shared adapter is ~7–9× slower; the shipped adapter reaches
`@CompileStatic` parity and ~2×
+under dynamic compilation. (Marking the adapter `GeneratedClosure` for the
metaclass fast path was
+evaluated and found perf-neutral once `doCall` was lean, so it is not
applied.) 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.
@@ -619,6 +629,16 @@ of a closure class's bytes were `Closure` scaffolding* —
constructor, metaclas
are the extreme case: a representative `RemarkCriteria` domain generated *24
closure classes
totalling 80 KB* — more bytecode than the entire rest of the class (1.19×).
+Dispatch speed matters as much as class count, because a shared adapter is
invoked through the
+`Closure` machinery rather than being its own class. A naive adapter
(reflective invoke plus per-call
+argument adaptation) measured ~7–9× slower to `call()` than the generated
closure class it replaces —
+smaller but slower. The shipped adapter closes that gap with a compile-time
`MethodHandle` constant
+invoked via `invokeExact` and a `doCall` fast path that skips adaptation on
the common exact-arity
+call: a packed `@CompileStatic` closure then dispatches at parity with a
normal closure, and a packed
+dynamic closure at ~2×. So `@CompileStatic` packing is strictly a win (fewer
classes, same call
+speed), while dynamic packing trades ~2× call cost for the class-count
reduction — which is why the
+dynamic path stays opt-in.
+
=== Grails artefact DSLs and nested closures
The delegate DSLs Grails relies on — `static constraints = { … }`, `static
mapping`,
@@ -656,7 +676,7 @@ suite before it lands.
|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)
+|3 |Closure packing (S1) + capability/escape analysis + `MethodHandle`
dispatch + `@PackedClosures` |Sound closure packing: automatic under
`@CompileStatic` at call-parity dispatch, 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
|===
@@ -703,10 +723,11 @@ 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 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.
+* Closure packing under `@CompileStatic` is a class-count win at dispatch
parity with a normal
+ closure: the typed hoisted body compiles statically, and the adapter reaches
it through a
+ `MethodHandle` constant (`invokeExact`) behind a lean `doCall` fast path.
Measured per `call()`, a
+ naive shared adapter is ~7–9× slower; the shipped one is at parity under
`@CompileStatic` and ~2×
+ under dynamic compilation (so the dynamic path stays opt-in).
=== Security