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

commit 69e0ff79bb7a0068792b2983056c6ff579c80518
Author: Paul King <[email protected]>
AuthorDate: Fri Jul 10 22:42:16 2026 +1000

    GEP-27: reconcile with implementation — @PackedClosures name and mode 
diagnostics
    
    Align the doc with what shipped:
    - rename @Packed -> @PackedClosures throughout (14 occurrences); the 
annotation is
      named for what it packs (closures), and since a dynamic arrow-lambda is 
itself a
      closure with no separate dynamic-lambda packing, the closures-specific 
name is
      exact -- note added;
    - replace the boolean @Packed(strict = true) design with the delivered
      @PackedClosures(mode = LENIENT | WARN | STRICT) enum (silent / compiler 
warning
      per decline with reason / compile error), and state that the automatic
      groovy.target.closure.pack path is always lenient;
    - drop 'designed but not yet wired' -- the mode diagnostics are implemented 
and
      listed among the delivered safety mechanisms.
---
 site/src/site/wiki/GEP-27.adoc | 57 +++++++++++++++++++++++-------------------
 1 file changed, 31 insertions(+), 26 deletions(-)

diff --git a/site/src/site/wiki/GEP-27.adoc b/site/src/site/wiki/GEP-27.adoc
index 7e38eac..ac4740c 100644
--- a/site/src/site/wiki/GEP-27.adoc
+++ b/site/src/site/wiki/GEP-27.adoc
@@ -212,9 +212,10 @@ _provably safe_ and _valuable_.
   already routes a SAM-targeted expression to the lighter (metafactory) 
writer; it is the
   programmer saying "treat me as a function." A brace closure and an arrow 
lambda targeting the
   _same_ functional interface compile differently today for exactly this 
reason.
-* *An opt-in annotation* (working name `@Packed`) is the same kind of thing: a 
programmer
-  assertion that a closure needs no external delegate, supplied where the 
compiler cannot prove
-  it (i.e. under dynamic compilation).
+* *The `@PackedClosures` annotation* is the same kind of thing: a programmer 
assertion that a
+  closure needs no external delegate, supplied where the compiler cannot prove 
it (i.e. under
+  dynamic compilation). It is named for what it packs — closures; a dynamic 
arrow-lambda is itself
+  a closure, and there is no separate dynamic-lambda packing, so the 
closures-specific name is exact.
 * *SAM vs not* is the target-type _upper bound_ on capability: a SAM target 
permits the
   object-free metafactory path; a `Closure`/`Object` target caps at the 
adapter.
 * *Capturing vs not* is a _cost dial_ (allocation vs singleton) within 
whatever strategy is
@@ -247,7 +248,7 @@ closure / lambda body
         v
  hoist to a method on the enclosing class      <-- shared backend
         |
-   capability inference  (target type; arrow/@Packed declarations;
+   capability inference  (target type; arrow/@PackedClosures declarations;
         |                  static-only escape + @DelegatesTo + serialization 
analysis)
         v
    +----+----+----------------------+---------------------+
@@ -296,7 +297,7 @@ it can land first.
 === Guarding the dynamic trust assertion
 
 Under `@CompileStatic` the escape analysis _proves_ delegate-independence and 
declines the rest,
-so the boundary is enforced by construction. Under dynamic compilation 
`@Packed` is a *trust
+so the boundary is enforced by construction. Under dynamic compilation 
`@PackedClosures` is a *trust
 assertion* the compiler cannot verify — so the design must protect a user who 
annotates in good
 faith and gets it wrong. Two mechanisms do this, and they deliberately cover 
*different* failure
 modes:
@@ -315,18 +316,21 @@ modes:
   so throwing is the only honest response. In the `@CompileStatic` case the 
guard should never fire
   (those closures are proven and declined), but keeping it is cheap 
defense-in-depth and documents
   the contract uniformly.
-* *`@Packed(strict = true)` (compile-time visibility, *not* a safety net).* 
Errors at compile time
-  if any closure in scope cannot be packed, so a decline is reported rather 
than silently yielding
-  a class. It surfaces the *visible* declines the compiler already knows about 
(local
-  `delegate`/`resolveStrategy` use, serialization, syntactic escape). It 
deliberately does *not*
-  protect against the wrong-assumption case above — an externally-set delegate 
is invisible at the
-  closure's definition site (that is the very reason dynamic packing needs a 
trust assertion), so
-  `strict` will report "packed" on exactly the closure the runtime guard later 
has to catch.
+* *`@PackedClosures(mode = ...)` (compile-time visibility, *not* a safety 
net).* Packing declines
+  are otherwise silent, so the annotation's `mode` element surfaces them: 
`LENIENT` (the default)
+  stays silent, `WARN` emits a compiler warning per unpacked closure naming 
the source position and
+  the reason, and `STRICT` turns any decline in scope into a compile error. It 
surfaces the *visible*
+  declines the compiler already knows about (a closure that resolves against a 
delegate, escapes its
+  method, or needs a real `Closure`). It deliberately does *not* protect 
against the wrong-assumption
+  case above — an externally-set delegate is invisible at the closure's 
definition site (the very
+  reason dynamic packing needs a trust assertion), so `WARN`/`STRICT` will 
report "packed" on exactly
+  the closure the runtime guard later has to catch. Only the annotation opts 
in; the automatic
+  `groovy.target.closure.pack` path is always lenient.
 
 [cols="1,1,3",options="header"]
 |===
 |Mechanism |When |Catches
-|`@Packed(strict = true)` |compile time |_visible_ declines — "did I pack what 
I intended?"
+|`@PackedClosures(mode = WARN or STRICT)` |compile time |_visible_ declines — 
"did I pack what I intended?"
 |`PackedClosure` throws on `setDelegate`/`setResolveStrategy` |runtime |the 
_invisible_ wrong assumption — "did anyone misuse what I packed?"
 |===
 
@@ -334,7 +338,7 @@ A conservative *escape gate* shrinks the runtime surface 
further and is implemen
 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
-a runtime throw to a compile-time non-event; those declines are also what 
`strict` would report.
+a runtime throw to a compile-time non-event; those declines are also what 
`WARN`/`STRICT` report.
 Because syntactic escape analysis cannot prove that, say, a custom `each` does 
not set a delegate,
 the runtime guard remains the essential backstop under dynamic compilation. (A 
closure that is
 fully self-contained — every free name binds to the owner or its own 
parameters — is
@@ -402,8 +406,8 @@ shared `Reference`, including `+=`/`++`), *arbitrary-depth 
nested closures*, imp
 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 and the compile-time escape decline 
(the optional
-`@Packed(strict = true)` enforcement is designed but not yet wired). What 
remains for a real feature
+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
@@ -440,14 +444,15 @@ means byte-for-byte today's behaviour.
 |Concern |Flag (proposed) |Default
 
 |Non-capturing SAM lambda hoisting (Groovy 6) 
|`groovy.target.lambda.hoist.disabled` |off (i.e. hoisting *on*), opt-out
-|Closure packing under `@CompileStatic` (Groovy 7) 
|`groovy.target.closure.pack` (+ per-scope `@Packed`) |to be decided per release
-|Closure packing under dynamic compilation |`@Packed` opt-in only |off unless 
annotated
+|Closure packing under `@CompileStatic` (Groovy 7) 
|`groovy.target.closure.pack` (+ per-scope `@PackedClosures`) |to be decided 
per release
+|Closure packing under dynamic compilation |`@PackedClosures` opt-in only |off 
unless annotated
 |===
 
-Under dynamic compilation `@Packed` carries an optional `strict` element
-(`@Packed(strict = true)`) that turns any *decline* in scope into a compile 
error, and every
-packed closure is backed by the `PackedClosure` runtime guard; see _Guarding 
the dynamic trust
-assertion_ for what each does and does not catch.
+`@PackedClosures` carries a `mode` element (`LENIENT` | `WARN` | `STRICT`, 
default `LENIENT`) that
+reports closures it could not pack — silently, as a compiler warning, or as a 
compile error; the
+automatic `groovy.target.closure.pack` path is always lenient. Every packed 
closure is backed by the
+`PackedClosure` runtime guard; see _Guarding the dynamic trust assertion_ for 
what each does and does
+not catch.
 
 The default *direction* is a policy decision independent of the mechanism: a 
feature can ship
 *on* with an opt-out flag (deliver the win immediately, keep a back-out 
valve), or *off* with an
@@ -545,7 +550,7 @@ development:
   assert the old bytecode shape need updating to the new structure (behaviour 
is unchanged); that
   test migration is the bulk of the remaining first-step work.
 * *Closure packing (Groovy 7 direction).* A `ClosureWriter`-based spike (with 
a shared
-  `PackedClosure` runtime adapter), opt-in via `@Packed`, demonstrates 
hoisting closures — captures,
+  `PackedClosure` runtime adapter), opt-in via `@PackedClosures`, demonstrates 
hoisting 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
@@ -611,7 +616,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
 |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 + `@Packed` |Sound 
closure packing: automatic under `@CompileStatic`, opt-in (with runtime guard + 
escape decline) under dynamic |Groovy 7.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
 |===
@@ -629,8 +634,8 @@ on the shared backend proven in phases 1–2.
 |Packing closures that use externally-set `delegate`/`resolveStrategy` |Not 
planned |These require a real per-closure representation; they are detected 
(statically) or declined and fall back to S0.
 |Serializable closure/lambda hoisting |Deferred |Kept on the class-based path 
initially; hoisting them requires carrying the deserialisation machinery to the 
enclosing class.
 |Object elision (S3) |Deferred (Groovy 7) |The most aggressive step; needs 
escape proof and, for iteration, callee knowledge.
-|Dynamic-mode automatic packing |Not planned |Cannot be proven sound without 
types; dynamic packing is opt-in via `@Packed` only.
-|A user-visible switch between S1/S2/S3 per closure |Not planned |The compiler 
picks the lightest sound representation; users influence it only via target 
type, arrow syntax, and `@Packed`.
+|Dynamic-mode automatic packing |Not planned |Cannot be proven sound without 
types; dynamic packing is opt-in via `@PackedClosures` only.
+|A user-visible switch between S1/S2/S3 per closure |Not planned |The compiler 
picks the lightest sound representation; users influence it only via target 
type, arrow syntax, and `@PackedClosures`.
 |===
 
 == Compatibility and impact

Reply via email to