You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by gi...@apache.org on 2026/07/15 03:33:41 UTC
(groovy-dev-site) branch asf-site updated: 2026/07/15 03:33:33: Generated dev website from groovy-website@18871b0
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-dev-site.git
The following commit(s) were added to refs/heads/asf-site by this push:
new 7e84f6a3 2026/07/15 03:33:33: Generated dev website from groovy-website@18871b0
7e84f6a3 is described below
commit 7e84f6a3d8e0334f1d774163e3c0826c6042a507
Author: jenkins <bu...@apache.org>
AuthorDate: Wed Jul 15 03:33:33 2026 +0000
2026/07/15 03:33:33: Generated dev website from groovy-website@18871b0
---
search/search-index.json | 4 +-
wiki/GEP-27.html | 163 +++++++++++++++++++++++++++++------------------
wiki/geps.html | 2 +-
3 files changed, 103 insertions(+), 66 deletions(-)
diff --git a/search/search-index.json b/search/search-index.json
index 2e9f3c18..45dbba65 100644
--- a/search/search-index.json
+++ b/search/search-index.json
@@ -891,7 +891,7 @@
{
"id": "wiki/GEP-27.html",
"title": "The Apache Groovy programming language - Developer docs - GEP-27",
- "content": "The Apache Groovy programming language - Developer docs - GEP-27 Socialize Discuss on the mailing list Groovy on X Groovy on Bluesky Groovy on Mastodon Groovy on LinkedIn Events and conferences Source code on GitHub Report issues in Jira Stack Overflow questions Slack Community You are using an outdated browser. Please upgrade your browser to improve your experience. Apache Groovy™ Learn Documentation Download Support Contribute Ecosystem Blog posts Socialize GE [...]
+ "content": "The Apache Groovy programming language - Developer docs - GEP-27 Socialize Discuss on the mailing list Groovy on X Groovy on Bluesky Groovy on Mastodon Groovy on LinkedIn Events and conferences Source code on GitHub Report issues in Jira Stack Overflow questions Slack Community You are using an outdated browser. Please upgrade your browser to improve your experience. Apache Groovy™ Learn Documentation Download Support Contribute Ecosystem Blog posts Socialize GE [...]
"url": "wiki/GEP-27.html",
"site": "dev"
},
@@ -933,7 +933,7 @@
{
"id": "wiki/geps.html",
"title": "The Apache Groovy programming language - GEPs",
- "content": "The Apache Groovy programming language - GEPs Socialize Discuss on the mailing list Groovy on X Groovy on Bluesky Groovy on Mastodon Groovy on LinkedIn Events and conferences Source code on GitHub Report issues in Jira Stack Overflow questions Slack Community You are using an outdated browser. Please upgrade your browser to improve your experience. Apache Groovy™ Learn Documentation Download Support Contribute Ecosystem Blog posts Socialize GEPs GEP-1 GEP-2 GEP- [...]
+ "content": "The Apache Groovy programming language - GEPs Socialize Discuss on the mailing list Groovy on X Groovy on Bluesky Groovy on Mastodon Groovy on LinkedIn Events and conferences Source code on GitHub Report issues in Jira Stack Overflow questions Slack Community You are using an outdated browser. Please upgrade your browser to improve your experience. Apache Groovy™ Learn Documentation Download Support Contribute Ecosystem Blog posts Socialize GEPs GEP-1 GEP-2 GEP- [...]
"url": "wiki/geps.html",
"site": "dev"
},
diff --git a/wiki/GEP-27.html b/wiki/GEP-27.html
index f45b8e69..1ee6c8ac 100644
--- a/wiki/GEP-27.html
+++ b/wiki/GEP-27.html
@@ -147,7 +147,7 @@
<strong>Last modification</strong>
</td>
<td class="hdlist2">
-<p>2026-07-13</p>
+<p>2026-07-15</p>
</td>
</tr>
</table>
@@ -222,9 +222,9 @@ per-closure class, while remaining a real <code>groovy.lang.Closure</code> (so <
<div class="paragraph">
<p>The proposal is deliberately phased. A <strong>small, sound-by-construction slice — eliminating the
generated class for non-capturing and read-only-capturing SAM lambdas — is merged for Groovy 6.0</strong>.
-<strong>Closure packing (S1), including its soundness analysis and a <code>MethodHandle</code>-based dispatch path that
-reaches <code>@CompileStatic</code> call-parity, is now implemented and proposed as a Groovy 6.0 stretch goal
-(GROOVY-12151)</strong>; the remaining, more
+<strong>Closure packing (S1), including its soundness analysis, a per-class dispatch-table path that
+outruns the generated classes it replaces on capturing shapes, and full MOP transparency, is now
+implemented and proposed as a Groovy 6.0 stretch goal (GROOVY-12151)</strong>; 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.</p>
@@ -401,7 +401,7 @@ has per-instance identity, and <code>curry</code>/<code>rcurry</code>/<code>ncur
<code>compose</code>, and iteration all continue to work (they operate through <code>call()</code>).</p>
</div>
<div class="paragraph">
-<p>A hoisted representation loses exactly two things relative to a normal generated closure:</p>
+<p>A hoisted representation loses exactly three things relative to a normal generated closure:</p>
</div>
<div class="ulist">
<ul>
@@ -411,8 +411,14 @@ time against the owner; a normal closure resolves free names against a <code>del
runtime. This is the one true soundness boundary (see <em>The soundness boundary</em>).</p>
</li>
<li>
-<p><strong>Serialization.</strong> A method-handle/dispatch-backed adapter is not portably serializable the way
-a generated closure class is.</p>
+<p><strong>Serialization.</strong> A dispatch-table-backed adapter is not portably serializable the way a
+generated closure class is.</p>
+</li>
+<li>
+<p><strong>Per-literal class identity.</strong> All packed closures share one adapter class, so
+<code>closure.getClass()</code> no longer distinguishes literals, and class-level metaclass changes on
+the adapter are global to packed closures (per-instance <code>setMetaClass</code> is fully honoured —
+see <em>MOP transparency</em>).</p>
</li>
</ul>
</div>
@@ -796,12 +802,31 @@ to S0 (a normal closure class) otherwise; under dynamic compilation it is opt-in
<li>
<p><strong>Fast dispatch</strong> — two parts. The hoisted body is typed from the inferred types the type checker
already computed and compiles statically (typed captures/params/return, zero <code>invokedynamic</code> in the
-body). And the shared adapter reaches that body through a <code>MethodHandle</code> constant to the private
-hoisted method, invoked with <code>invokeExact</code> (no reflection), behind a <code>doCall</code> fast path that skips
-argument adaptation on the common exact-arity call. Measured per <code>call()</code> against the generated
-closure class it replaces: a naive shared adapter is ~7–9× slower, whereas the shipped one reaches
-<code>@CompileStatic</code> parity and ~2× under dynamic compilation — so packing is a class-count win at no
-dispatch-speed penalty under <code>@CompileStatic</code>.</p>
+body). And the shared adapter reaches that body through compiler-generated per-class <strong>dispatch
+tables</strong>: a general <code>(int id, Object[])</code> table covering every hoisted target plus array-free
+per-arity tables for targets taking one or two values beyond the receiver (captures count as
+values — the overwhelmingly common shapes), each case casting to the declared parameter types and
+invoking the target directly. One <code>invokedynamic</code> accessor per class links the tables into a
+constant bundle lazily on first use, and each literal’s parameter-type <code>Class[]</code> is a
+constant-dynamic resolved once per site. The whole chain is ordinary, JIT-friendly bytecode — no
+reflection, and no per-instance <code>MethodHandle</code> (not constant-foldable, hence many times slower).
+Measured with JMH against the generated classes it replaces (same build, both compilations):
+capturing shapes run <strong>1.4–1.9× faster packed</strong> — the win holding under megamorphic dispatch across
+many classes — while the tightest non-capturing <code>collect</code> trails at ~0.6×; on Grails-shaped
+in-situ workloads (domain pipelines, validation cycles) the blend nets <strong>+17–23% packed</strong> with
+identical outputs, so the adverse micro-shape dilutes out under real work.</p>
+</li>
+<li>
+<p><strong>MOP transparency</strong> — the direct dispatch path is semantically invisible: it is taken only when the
+instance’s metaclass is stock and no category is active (one latched-boolean check, the same
+conditions the classic call-site caches test), and anything MOP-relevant routes through
+<code>invokeMethod</code>. Interception via a per-instance metaclass is honoured identically on the dynamic
+and Java/GDK paths, matching generated closure classes. The adapter has its own registered stock
+metaclass, <code>PackedClosureMetaClass</code> — the analog of <code>ClosureMetaClass</code> for generated closures —
+giving reflection-free <code>call</code>/<code>doCall</code> dispatch on the MOP route, <code>respondsTo</code> answers faithful to
+the instance’s declared parameter types, and the same category stance as <code>ClosureMetaClass</code>. (The
+<code>GeneratedClosure</code> marker is deliberately not used: its metaclass machinery assumes an inner class
+of the owner, which a shared top-level adapter is not.)</p>
</li>
</ul>
</div>
@@ -851,13 +876,13 @@ proof and, for iteration intrinsics, knowledge of the callee.</p>
<p>A related follow-up surfaced in review: <code>MethodClosure</code> (from <code>.&</code> method references) is the same
shape — a <code>Closure</code> whose target is a known method that cannot honour a delegate — but it dispatches
by name with runtime overload selection and <strong>silently ignores</strong> a caller-set delegate, whereas
-<code>PackedClosure</code> binds a fixed <code>MethodHandle</code> and <strong>rejects</strong> one. The two are worth <em>contract</em>-aligning
-(a shared "known-target, delegate-inert" marker and consistent delegate handling), and both could
-share the per-arity handle-selection primitive that default-parameter packing (see <em>Excluded and
-deferred features</em>) needs — the same mechanism that would close the remaining dynamic-dispatch
-throughput gap (see <em>Performance</em>). Their dispatch models differ enough, though — a fixed handle versus
-runtime overload selection — that fully merging them into one path is not a goal; this is tracked as a
-separate initiative.</p>
+<code>PackedClosure</code> dispatches through a fixed table entry and <strong>rejects</strong> one. The two are worth
+<em>contract</em>-aligning (a shared "known-target, delegate-inert" marker and consistent delegate
+handling), and statically resolved method pointers could claim ids in the same per-class dispatch
+tables the packed adapter already uses — dispatching with the host class’s own bytecode rights,
+which also gives caller-sensitivity-correct semantics. Their dispatch models differ enough, though —
+a fixed table entry versus runtime overload selection — that fully merging them into one path is not
+a goal; this is tracked as a separate initiative.</p>
</div>
</div>
<div class="sect2">
@@ -866,7 +891,9 @@ separate initiative.</p>
<p>Flip closure packing from experimental (opt-in <code>groovy.target.closure.pack</code>) to default-on. This is
a policy decision gated on the remaining human-only tooling verification (see <em>IDE and tooling
considerations</em>) and on migrating the tests that assert the old bytecode shape — not on further
-mechanism work.</p>
+mechanism work: the dispatch and MOP-transparency mechanisms are complete and verified (interception,
+categories, and introspection behave identically to generated closure classes on every path), and the
+remaining documented residuals are those listed under <em>The one that matters: required capability</em>.</p>
</div>
</div>
</div>
@@ -1112,16 +1139,19 @@ a scope out entirely. The automatic flag path stays lenient.</p>
</div>
<div class="paragraph">
<p>Also implemented and validated: the <strong>capability analysis</strong> (STC’s delegate-independence proof driving
-automatic packing under <code>@CompileStatic</code>, no annotation needed) and the <strong>dispatch path</strong> — a
-statically-compiled, typed hoisted body reached through a <code>MethodHandle</code> constant to the private
-hoisted method (<code>invokeExact</code>, no reflection) behind a lean <code>doCall</code> fast path — exercised across the
-core module and several others. Measured per <code>call()</code> against the generated closure class it replaces,
-a naive shared adapter is ~7–9× slower; the shipped adapter reaches <code>@CompileStatic</code> parity and ~2×
-under dynamic compilation. (Marking the adapter <code>GeneratedClosure</code> for the metaclass fast path was
-evaluated and found perf-neutral once <code>doCall</code> 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.</p>
+automatic packing under <code>@CompileStatic</code>, no annotation needed); the <strong>dispatch path</strong> — a
+statically-compiled, typed hoisted body reached through the per-class dispatch tables described under
+<em>Groovy 6.0 deliverables</em>, behind lean adapter lanes sized to the JIT’s inlining budgets — exercised
+across the core module and several others; and the <strong>MOP integration</strong> — the stock-metaclass/category
+guard shared with <code>Closure.call’s own fast-path cache, and the registered `PackedClosureMetaClass</code>
+(reflection-free MOP dispatch, instance-faithful <code>respondsTo</code>), with the interception matrix verified
+identical to generated closure classes on both the dynamic and Java/GDK paths. Measured with JMH
+against the generated classes it replaces, capturing shapes run 1.4–1.9× faster packed and
+Grails-shaped in-situ workloads net +17–23%, with byte-identical corpus transcripts. 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.</p>
</div>
<div class="sect2">
<h3 id="_measured_impact">Measured impact</h3>
@@ -1137,13 +1167,20 @@ totalling 80 KB</strong> — more bytecode than the entire rest of the class (1.
<div class="paragraph">
<p>Dispatch speed matters as much as class count, because a shared adapter is invoked through the
<code>Closure</code> machinery rather than being its own class. A naive adapter (reflective invoke plus per-call
-argument adaptation) measured ~7–9× slower to <code>call()</code> than the generated closure class it replaces —
-smaller but slower. The shipped adapter closes that gap with a compile-time <code>MethodHandle</code> constant
-invoked via <code>invokeExact</code> and a <code>doCall</code> fast path that skips adaptation on the common exact-arity
-call: a packed <code>@CompileStatic</code> closure then dispatches at parity with a normal closure, and a packed
-dynamic closure at ~2×. So <code>@CompileStatic</code> 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.</p>
+argument adaptation) measures ~7–9× slower to <code>call()</code> than the generated closure class it replaces —
+smaller but slower — and a per-instance <code>MethodHandle</code> fares little better (the JIT cannot
+constant-fold it, so every call runs the method-handle invoker). The shipped design avoids both: the
+per-class dispatch tables execute as ordinary bytecode the JIT inlines through, the per-arity tables
+pass arguments as plain parameters (no array to allocate or escape), and constant-dynamic
+parameter-type arrays remove the last per-creation allocation. Measured with JMH on the same build,
+capturing closures — the shapes GEP-27 exists for — run <strong>1.4–1.9× faster packed than as generated
+classes</strong>, holding under megamorphic dispatch; the tightest non-capturing <code>collect</code> trails at ~0.6×,
+the cost being the MOP-transparency guard (loop-invariant checks the JIT hoists when the chain
+inlines — the same checks the classic call-site caches make). On Grails-shaped in-situ workloads the
+blend nets <strong>+17–23% packed</strong> with identical outputs, so the adverse micro-shape is real but dilutes
+out under real work (further lane micro-tuning was assessed against those shapes and found to have no
+in-situ effect). Ratios are measured against a current baseline that includes the <code>Closure.call</code>
+fast-path improvements (GROOVY-12164/12165), i.e. against generated classes at their fastest.</p>
</div>
</div>
<div class="sect2">
@@ -1172,11 +1209,11 @@ owner-correct capability work already scoped to Groovy 7. This makes domain clas
highest-value target for that phase, and the reason it is Groovy 7 and not a cheap gate change.</p>
</div>
<div class="paragraph">
-<p>A supporting change surfaced by the closure spike is worth noting: hoisting nested closures also
-requires the class visitor to visit methods added <em>during</em> code generation to a fixpoint (the
-current two-round <code>visitMethods</code> misses methods added while compiling a method that was itself added
-during compilation). That change is general and low-risk but must be validated against the full
-suite before it lands.</p>
+<p>A supporting change is worth noting: hoisting nested closures requires the class visitor to visit
+methods added <em>during</em> code generation to a fixpoint (a two-round <code>visitMethods</code> misses methods
+added while compiling a method that was itself added during compilation). That change is implemented
+in the packing slice — an identity-based fixpoint, also fixing an O(n²) scan against
+method-generating visitors — and is validated against the full suite.</p>
</div>
</div>
<div class="sect2">
@@ -1211,8 +1248,8 @@ suite before it lands.</p>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">3</p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">Closure packing (S1) + capability/escape analysis + <code>MethodHandle</code> dispatch + <code>@PackedClosures</code></p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">Sound closure packing: automatic under <code>@CompileStatic</code> at call-parity dispatch, opt-in (with runtime guard + escape decline) under dynamic</p></td>
+<td class="tableblock halign-left valign-top"><p class="tableblock">Closure packing (S1) + capability/escape analysis + per-class dispatch tables + MOP transparency + <code>@PackedClosures</code></p></td>
+<td class="tableblock halign-left valign-top"><p class="tableblock">Sound closure packing: automatic under <code>@CompileStatic</code>, faster than the classes it replaces on capturing shapes, opt-in (with runtime guard + escape decline) under dynamic</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Groovy 6.0 (stretch, GROOVY-12151)</p></td>
</tr>
<tr>
@@ -1271,7 +1308,7 @@ stretch goal) builds on the shared backend proven in phases 1–2; phases 4–5
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Packing default-parameter (multi-arity) closures</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Deferred</p></td>
-<td class="tableblock halign-left valign-top"><p class="tableblock">A single hoisted method cannot reproduce the arity-overloaded dispatch a default parameter generates, so these decline to S0 today. A future slice would hoist one method per arity (reusing the existing default-fill codegen) and have the adapter select a <code>MethodHandle</code> per arity at call time — the same primitive the <em>MethodClosure</em> alignment above would share.</p></td>
+<td class="tableblock halign-left valign-top"><p class="tableblock">A single hoisted method cannot reproduce the arity-overloaded dispatch a default parameter generates, so these decline to S0 today. A future slice would hoist one method per arity (reusing the existing default-fill codegen) and claim one dispatch-table id per arity — the per-arity tables the shipped adapter already uses, and the same primitive the <em>MethodClosure</em> alignment above would share.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Object elision (S3)</p></td>
@@ -1338,27 +1375,27 @@ the compatibility valve while tools are updated.</p>
is dead weight, not an allocation).</p>
</li>
<li>
-<p>Closure packing under <code>@CompileStatic</code> 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
-<code>MethodHandle</code> constant (<code>invokeExact</code>) behind a lean <code>doCall</code> fast path. Measured per <code>call()</code>, a
-naive shared adapter is ~7–9× slower; the shipped one is at parity under <code>@CompileStatic</code> and ~2×
-under dynamic compilation (so the dynamic path stays opt-in).</p>
+<p>Closure packing dispatches through per-class tables that execute as ordinary, JIT-inlinable
+bytecode; the per-arity tables pass the receiver, captures and arguments as plain parameters, so
+the hot shapes allocate no argument array at all, and allocation sits at or below the
+closure-class baseline on capturing shapes.</p>
+</li>
+<li>
+<p>Measured with JMH against the generated classes on the same build: capturing shapes 1.4–1.9×
+faster packed (holding under megamorphic dispatch); tight non-capturing <code>collect</code> ~0.6× — the
+MOP-transparency guard’s loop-invariant checks, hoisted to zero when the chain inlines. On
+Grails-shaped in-situ workloads the blend nets +17–23% packed with identical outputs.</p>
</li>
<li>
-<p>Capturing closures (the common case, e.g. <code>{ it + base }</code>) used to pay an extra per-call cost — the
-adapter allocated an argument array prepending the captures on every invocation. Binding the captures
-into the handle once at construction removes that allocation (GROOVY-12151). Measured per <code>call()</code>
-against the generated closure class it replaces, a capturing closure improved from dynamic ~3.2× /
-<code>@CompileStatic</code> ~1.75× to dynamic ~1.9× / <code>@CompileStatic</code> ~0.78× (faster than a normal closure under
-<code>@CompileStatic</code>), matching the non-capturing figures above.</p>
+<p>MOP transparency has the same cost model as the classic call-site caches: one latched-boolean and
+one category-counter read decide between direct dispatch and the full <code>invokeMethod</code> route, so
+interception, categories and <code>respondsTo</code> behave identically to generated closure classes.</p>
</li>
<li>
-<p>The remaining ~2× dynamic gap is the argument-array spreader plus the dynamic MOP <code>call</code>→<code>doCall</code>
-selection (<code>PackedClosure</code> is not a <code>GeneratedClosure</code>) — no longer allocation. A typed per-arity
-<code>invokeExact</code> path would remove the spreader for the common <code>each</code>/<code>collect</code> arities: the <strong>same</strong>
-per-arity handle-selection primitive that default-parameter packing and the MethodClosure alignment
-need (see <em>Unify the two writers</em> and <em>Excluded and deferred features</em>), so that throughput work and
-those follow-ons build one shared mechanism.</p>
+<p>Under strict JPMS (named modules, no <code>--add-opens</code>), packed dispatch works where reflective
+dispatch of closure classes does not: the hosting class’s own <code>invokedynamic</code> bootstrap captures
+its lookup — the capability model Java lambdas use — and handing the closure across a module
+boundary is the grant. This deliberately moves enforcement from invocation time to creation time.</p>
</li>
</ul>
</div>
diff --git a/wiki/geps.html b/wiki/geps.html
index f83d6530..e006fb5b 100644
--- a/wiki/geps.html
+++ b/wiki/geps.html
@@ -63,7 +63,7 @@
</ul>
</div>
</div>
- </div><div id='content' class='page-1'><div class='row'><div class='row-fluid'><div class='col-lg-3'><ul class='nav-sidebar'><li class='active'><a href='#gep'>GEPs</a></li><li><a href='#GEP-1' class='anchor-link'>GEP-1</a></li><li><a href='#GEP-2' class='anchor-link'>GEP-2</a></li><li><a href='#GEP-3' class='anchor-link'>GEP-3</a></li><li><a href='#GEP-4' class='anchor-link'>GEP-4</a></li><li><a href='#GEP-5' class='anchor-link'>GEP-5</a></li><li><a href='#GEP-6' [...]
+ </div><div id='content' class='page-1'><div class='row'><div class='row-fluid'><div class='col-lg-3'><ul class='nav-sidebar'><li class='active'><a href='#gep'>GEPs</a></li><li><a href='#GEP-1' class='anchor-link'>GEP-1</a></li><li><a href='#GEP-2' class='anchor-link'>GEP-2</a></li><li><a href='#GEP-3' class='anchor-link'>GEP-3</a></li><li><a href='#GEP-4' class='anchor-link'>GEP-4</a></li><li><a href='#GEP-5' class='anchor-link'>GEP-5</a></li><li><a href='#GEP-6' [...]
<div class='row'>
<div class='colset-3-footer'>
<div class='col-1'>