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 e92adb5 updated with recent changes for GROOVY-12151
e92adb5 is described below
commit e92adb5a8dfe358308490020f9cb212b80a838ac
Author: Paul King <[email protected]>
AuthorDate: Tue Jul 21 18:30:08 2026 +1000
updated with recent changes for GROOVY-12151
---
site/src/site/wiki/GEP-27.adoc | 46 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 45 insertions(+), 1 deletion(-)
diff --git a/site/src/site/wiki/GEP-27.adoc b/site/src/site/wiki/GEP-27.adoc
index d321181..0b68c0b 100644
--- a/site/src/site/wiki/GEP-27.adoc
+++ b/site/src/site/wiki/GEP-27.adoc
@@ -14,7 +14,7 @@
*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-16
+*Last modification*:: 2026-07-17
****
[width="80%",align="center"]
@@ -604,6 +604,50 @@ which also gives caller-sensitivity-correct semantics.
Their dispatch models dif
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.
+=== OpenClosure: a lexically bound core with a resolver boundary (reference
spike)
+
+Out of the GROOVY-12151 review discussion came a proposal (Jochen) for where
this architecture
+should ultimately land: split a closure into a *core* that binds all lexical
names at creation
+(receiver, captures, body) and a single pluggable *resolver* that answers only
for free names —
+the MOP reduced to exactly the dynamic part. The classic `groovy.lang.Closure`
contract
+(owner/delegate/`resolveStrategy`) becomes a resolver implementation plus a
wrapper over the
+core; under static compilation the resolver is provably empty; and the core —
"OpenClosure" —
+is a candidate to become the default closure construct, with `Closure` and
`OpenClosure`
+coexisting (`Closure` wrapping an `OpenClosure` at its heart).
+
+A working *reference spike* for Groovy 7 exists (branch
`gep27spikeOpenClosure`, behind
+`groovy.spike.openclosure`; not production code):
+
+* *Compiler:* a dynamic closure whose only obstacle is free names — exactly
what GROOVY-12151
+ must decline — hoists through the unchanged packed pipeline with its free
reads and
+ unqualified calls rewritten into `resolver.property(name)` /
`resolver.invoke(name, args)` on
+ a leading `Resolver` parameter; the resolver rides as `captured[0]`, so the
dispatch tables,
+ ids and capture threading need no modification.
+* *Runtime:* `OpenClosure` (the pure core, not a `Closure`),
`OpenClosure.Resolver` (the
+ boundary; `EMPTY` for the static case), `ClassicResolver` (the
owner/delegate/strategy triple
+ as a resolver), and `OpenClosure.AsClosure` — the coexistence wrapper, which
*extends*
+ `PackedClosure`: `setDelegate`/`setResolveStrategy` reconfigure the resolver
instead of
+ failing fast.
+* *Validated:* full behavioural parity with classed compilation across the
delegate/strategy
+ matrix (owner-first, delegate-first with full and partial interception,
delegate-only
+ including exception parity, owner-only) — checked against the classed run of
the same source,
+ not hardcoded expectations; one per-class dispatch table serving ordinary
packed and open
+ bodies side by side; GDK iteration and `curry` on the wrapper; and the
"template" reading of
+ free names — the same hoisted body executed under two different resolvers
yields two different
+ meanings, the dynamic part supplied entirely from outside.
+
+The alignment findings the spike was built to test: the GROOVY-12151 machinery
*is* the
+OpenClosure engine (no dispatch changes were needed); a packed closure is an
open closure whose
+resolver is statically known to be empty — the strict runtime guard is
precisely the degenerate
+resolver that forbids anyone supplying meanings; and the classic contract
reconstructs cleanly
+at the resolver boundary. Free-name <em>writes</em> are covered too: the
resolver's
+`setProperty(name, value)` leg handles plain and compound assignment (`total
+= it`) with the
+same read-then-write MOP pair and strategy order as a classic closure,
preserving
+assignment-expression value semantics. Deliberately out of the spike's scope
(production work,
+tracked for the design discussion): `++`/`--` on free names, `this`-qualified
accesses, nested
+literals inside open bodies, the `@DelegatesTo`-typed static variant,
per-arity class-level
+presentation of the wrapper, and the serialization posture of resolvers.
+
=== Default-on hardening
Flip closure packing from experimental (opt-in `groovy.target.closure.pack`)
to default-on. This is