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/04/25 20:45:14 UTC

(groovy-website) branch asf-site updated (1b9d0cc -> af70f30)

This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a change to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git


    from 1b9d0cc  draft GEP for additional multi-assignment forms (minor tweaks)
     new 5fbacb6  draft GEP for additional multi-assignment forms (minor tweaks)
     new af70f30  draft GEP for switch patterns

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 site/src/site/wiki/GEP-19.adoc | 677 +++++++++++++++++++++++++++++++++++++++++
 site/src/site/wiki/GEP-20.adoc |   4 +-
 2 files changed, 679 insertions(+), 2 deletions(-)
 create mode 100644 site/src/site/wiki/GEP-19.adoc


(groovy-website) 02/02: draft GEP for switch patterns

Posted by pa...@apache.org.
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 af70f305bde71168f676a94fe7638464dccb5f36
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Apr 26 06:45:05 2026 +1000

    draft GEP for switch patterns
---
 site/src/site/wiki/GEP-19.adoc | 677 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 677 insertions(+)

diff --git a/site/src/site/wiki/GEP-19.adoc b/site/src/site/wiki/GEP-19.adoc
new file mode 100644
index 0000000..c546e3e
--- /dev/null
+++ b/site/src/site/wiki/GEP-19.adoc
@@ -0,0 +1,677 @@
+= GEP-19: Structural Pattern Matching in switch
+
+:icons: font
+
+.Metadata
+****
+[horizontal,options="compact"]
+*Number*:: GEP-19
+*Title*:: Structural Pattern Matching in switch
+*Version*:: 1
+*Type*:: Feature
+*Status*:: Draft
+*Leader*:: Paul King
+*Created*:: 2026-04-25
+*Last modification*&#160;:: 2026-04-25
+****
+
+[width="80%",align="center"]
+|===
+a| NOTE: _WARNING:_
+Material on this page is still under development!
+We are currently working Groovy 6.0 and this proposal 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, future align design
+decisions in Groovy 6 as best we can, and iterate on the design.
+We welcome feedback and discussion, but please keep in mind that the details are not yet finalized.
+|===
+
+== Abstract
+
+This GEP extends Groovy's `switch` expression with structural pattern
+matching for lists, maps, and types, building on the record patterns
+introduced in 5.0 and aligning with Java's pattern-matching trajectory
+(JEPs 440, 441, 456, 507).
+
+The proposal adds list patterns (`[var h, var... t]`), map patterns
+(`[name: var n, var... rest]`), type patterns with binding (`String s`),
+guard clauses (`when`), and a unified wildcard form (`_` inside record
+patterns; `var _` elsewhere). The same pattern grammar is also accepted
+in `def [...] = ...` bracket-form declarations, providing a consistent
+destructuring grammar between switch case labels and assignment for
+users who want it. The everyday parens form (`def (...) = ...`) is
+covered separately by GEP-20 and ships in Groovy 6.x.
+
+All existing `switch` semantics are preserved: legacy `isCase` matching
+(constants, ranges, regex, classes, collections, maps, closures) compiles
+exactly as it does today. Pattern matching is opt-in via the binding
+markers that distinguish a structural pattern from a legacy case label.
+
+== Motivation
+
+Groovy's `switch` already exceeds Java's classic switch through the
+`isCase` protocol — case labels can be classes, ranges, regex patterns,
+collections, and closures. Groovy 5.0 added record patterns aligned with
+Java 21's JEP 440. What remains absent is structural decomposition of
+the most common data shapes: lists, maps, and arrays.
+
+Languages that have shipped this — Scala, Rust, Swift, Kotlin (limited),
+JavaScript (via destructuring), Python (via PEP 634), and Jactl — show
+that destructuring in switch / match is one of the most-used pattern
+matching idioms once available. Without it, the canonical functional
+expression of recursive list algorithms remains awkward in Groovy:
+
+[source,groovy]
+----
+// Without structural matching
+def qsort(list) {
+    if (list.size() <= 1) return list
+    def h = list[0]
+    def t = list.drop(1)
+    qsort(t.findAll { it < h }) + [h] + qsort(t.findAll { it >= h })
+}
+
+// With structural matching (this proposal)
+def qsort(x) {
+    switch (x) {
+        case []                -> []
+        case [var h, var... t] -> qsort(t.findAll { it < h }) + [h] + qsort(t.findAll { it >= h })
+    }
+}
+----
+
+Java is moving in the same direction. Beyond what has shipped, JEP
+drafts cover deconstructor methods for arbitrary classes, primitive
+patterns in `switch` (JEP 507 preview), and array patterns under
+discussion. Aligning Groovy's surface syntax with Java's where they
+overlap — and lowering through a uniform internal representation —
+keeps Groovy forward-compatible without paying for it twice.
+
+=== Design principles
+
+* *No legacy regression* — every program valid under Groovy 6 compiles
+  with identical semantics in 7. The disambiguation between legacy
+  `isCase` and structural patterns is parser-decidable and based solely
+  on the presence of binding markers.
+
+* *Java alignment where syntax overlaps* — `when` guards, type patterns,
+  record patterns, and `_` unnamed pattern syntax match Java verbatim.
+  Rest bindings reuse Groovy's existing varargs syntax (`Type... ident`),
+  the form Java is most likely to adopt for variadic deconstructor
+  components.
+
+* *Groovy-native where Java has no analogue* — list and map literal
+  patterns (`[...]`, `[k: v]`) have no Java counterpart because Java
+  has no list or map literals. Groovy can lead here without future
+  drift risk. In all binding positions, `var` and `def` are
+  interchangeable (matching Groovy's existing local-variable
+  convention and GEP-20), and rest slots accept the shortcut `...`
+  (or `... ident`) for `var... _` (or `var... ident`).
+
+* *Shared bracket-form grammar with assignment* — the pattern grammar
+  is accepted by `def [...] = expr` declarations as by
+  `case [...] ->` labels. The parens form (`def (...) = ...`) covered
+  by GEP-20 remains the canonical surface for everyday destructuring;
+  the bracket form is the opt-in bridge to switch pattern grammar.
+
+* *Forward-compatible lowering* — list and map patterns desugar through
+  an internal `Deconstructable` strategy. When Java's deconstructor JEP
+  ships, surface forms like `case List.of(var h, var... t) ->` slot in
+  as additional spellings of the same lowering — no architectural
+  change needed.
+
+== Features
+
+=== Type patterns with binding
+
+A type pattern matches if the switch value is an instance of the named
+type, binding a new local variable in the case body:
+
+[source,groovy]
+----
+switch (obj) {
+    case String s   -> s.toUpperCase()
+    case Integer i  -> i * 2
+    case Number n   -> n.doubleValue()
+    default         -> null
+}
+----
+
+The bound name is narrowed to the declared type within the case body
+and any `when` guard. `@CompileStatic` and `@TypeChecked` see the
+narrowed type via the same flow analysis already used for record
+patterns.
+
+=== Record patterns
+
+Record patterns (introduced in Groovy 5.0) deconstruct record-typed
+values positionally. Component positions accept nested patterns,
+including the unnamed pattern `_`:
+
+[source,groovy]
+----
+record Point(int x, int y) {}
+record Line(Point start, Point end) {}
+
+switch (obj) {
+    case Point(int x, int y)         -> "$x,$y"
+    case Point(int x, _)             -> "x=$x"
+    case Line(Point(_, _), Point p2) -> "ends at $p2"
+}
+----
+
+Bare `_` is permitted inside `Type(...)` because record-pattern
+component grammar is not expression grammar — there is no collision
+with `_` as an identifier in expression position.
+
+=== List patterns
+
+List patterns destructure `List`, array, and `Iterable` values
+structurally. Element positions accept literals, type patterns,
+`var`/`def` bindings, nested patterns, and a single rest binding:
+
+[source,groovy]
+----
+switch (xs) {
+    case []                                     -> "empty"
+    case [...]                                  -> "non-empty list (any shape)"
+    case [var only]                             -> "single: $only"
+    case [var h, var... t]                      -> "h=$h, t=$t"
+    case [def h, ... t]                         -> "h=$h, t=$t (using shortcuts)"
+    case [Integer h, var... t]                  -> "int head $h"
+    case [var first, var... middle, var last]   -> "$first..$last"
+    case [1, var x, ...]                        -> "starts with 1, then $x"
+}
+----
+
+`var` and `def` are interchangeable in any binding position. The
+shortcut `...` is accepted for `var... _` (and `... ident` for
+`var... ident`); see _Rest bindings_ below.
+
+Element count rules:
+
+* Without rest: a pattern of `n` elements matches values of exactly
+  `n` elements.
+* With one rest: a pattern of `n` fixed elements plus a rest binding
+  matches values of at least `n` elements. Rest may appear in any
+  single position. Multiple rest bindings at the same level are a
+  compile error.
+
+Type rules:
+
+* For `List<T>` input, untyped element bindings are inferred as `T`,
+  the rest binding as `List<T>`.
+* For `T[]` input, the rest binding is inferred as `T[]`.
+* For other `Iterable<T>` input, the iterable is materialised as a list
+  once for matching.
+* For `Object` input, bindings fall back to `Object` and `List`.
+
+=== Map patterns
+
+Map patterns destructure `Map` values by key. Keys are compile-time
+constant expressions; values are arbitrary patterns:
+
+[source,groovy]
+----
+switch (m) {
+    case [name: var n, age: var a]       -> "person $n, $a"
+    case [type: 'circle', radius: var r] -> "circle r=$r"
+    case [name: String n, ... rest]      -> "named $n; others=$rest"
+    case [name: def n, ...]              -> "any named map (others discarded)"
+}
+----
+
+Map pattern semantics are *open*: a pattern matches if all named keys
+are present and their value patterns match. Extra keys in the map are
+ignored unless captured by a rest binding. Closed semantics — "exactly
+these keys" — is expressed via a guard:
+
+[source,groovy]
+----
+case [name: var n] when ((Map) m).size() == 1 -> ...
+----
+
+The rest binding `var... rest` in a map pattern binds a `Map` of the
+entries not matched by named keys.
+
+=== Empty literals
+
+The empty list literal `[]` and empty map literal `[:]` in case-label
+position are always treated as patterns matching empty collections of
+the appropriate kind:
+
+[source,groovy]
+----
+case []   -> "empty list"
+case [:]  -> "empty map"
+----
+
+The legacy `isCase` semantics for these — never matching anything,
+because `[].contains(x)` and `[:].get(x)` are always false / null —
+have no practical use, so claiming the pattern interpretation removes
+no functionality.
+
+=== Wildcards
+
+The unnamed pattern matches any value without binding it. The form
+depends on context:
+
+[cols="2,1,3"]
+|===
+|Position |Form |Reason
+
+|Inside `Type(...)` record patterns
+|`_`
+|No expression-grammar collision; matches Java 22 (JEP 456)
+
+|Inside `[...]` list and `[k: v]` map patterns
+|`var _` or `def _`
+|Bare `_` is a legal identifier in expression grammar today; explicit
+ `var _` / `def _` avoids re-interpreting valid programs
+
+|Rest discard
+|`...` (shortcut) or `var... _` / `def... _` (canonical)
+|See _Rest bindings_ below
+
+|Top-level case label
+|`default ->` (preferred) or `case var _ ->`
+|Bare `case _ ->` retains its legacy meaning when `_` is in scope as
+ an identifier
+|===
+
+=== Rest bindings
+
+Rest bindings collect remaining elements into a single binding. The
+canonical form is `var... ident` (or `def... ident` / `Type... ident`),
+reusing Groovy's existing varargs token sequence — the same shape as
+`int... args` in method parameter lists:
+
+[source,groovy]
+----
+case [var h, var... t]      -> ...   // canonical
+case [def h, def... t]      -> ...   // equivalent (var/def interchangeable)
+case [var h, Integer... t]  -> ...   // typed rest
+case [var h, var... _]      -> ...   // discarded rest, canonical
+----
+
+The `var... t` spelling reuses an existing Groovy token sequence
+(varargs in method declarations) and matches the shape Java is most
+likely to adopt for variadic deconstructor components — keeping the
+`case List.of(var h, var... t)` surface form (when Java specifies
+it) consistent with the list-literal form.
+
+==== `...` shortcut
+
+The triple-dot `...` is accepted as a shortcut wherever `var...` (or
+`def...`) appears, reflecting that the leading `var` / `def` is
+ceremonial once `...` has signalled the rest position:
+
+[source,groovy]
+----
+case [var h, ... t]         -> ...   // shortcut for `var... t`
+case [var h, ...]           -> ...   // shortcut for `var... _`
+case [...]                  -> ...   // matches any list
+----
+
+Both `... ident` and bare `...` flip a `[...]` from legacy to pattern
+interpretation on their own. The `...` token has no expression-position
+meaning today (it is reserved only for varargs in method declarations
+and enhanced-for index variables), so claiming it as pattern grammar
+reinterprets no existing program.
+
+Bare `...` matches any list (including empty), since the rest can
+absorb zero elements. It pairs naturally with `case [] ->`:
+
+[source,groovy]
+----
+case []     -> "empty"
+case [...]  -> "non-empty (the empty case is matched above)"
+----
+
+The typed shortcut `Integer... t` is *not* further shortened to
+`... t`, because the type ascription carries semantic content (a
+runtime element-type check) that bare `...` would discard.
+
+For reference, GEP-20's parens-form `def (...) = ...` uses `\*ident`
+and `*_` for the same role — the parens-form analogue of
+`var... ident` and `var... _` (or the `...` shortcut). The `*`
+spelling is not adopted inside `[...]` patterns here because `\*`
+collides with list-literal spread in expression position; see
+_Excluded and deferred features_ for the relationship between the
+two and the conditions under which the parser could in principle
+accept the `*` spelling inside an already-disambiguated pattern.
+
+=== Guards
+
+`when` guards apply to patterns:
+
+[source,groovy]
+----
+case Integer i when i > 0                     -> "positive"
+case [var h, var... t] when t.size() > 5      -> "long list head=$h"
+case Point(int x, int y) when x == y          -> "diagonal point"
+----
+
+Guards may reference any binding from the same pattern. Guards are
+evaluated once after pattern matching succeeds; arms with failing
+guards fall through to the next arm.
+
+=== Patterns in `instanceof`
+
+Type patterns and record patterns extend to `instanceof`, mirroring
+Java:
+
+[source,groovy]
+----
+if (obj instanceof String s) {
+    println s.length()
+}
+if (point instanceof Point(int x, int y)) {
+    println "$x, $y"
+}
+----
+
+List and map patterns are not valid in `instanceof` because they have
+no type at the head. To test "is this value shaped like ...", use a
+single-arm `switch` expression returning a boolean.
+
+=== Disambiguation rule
+
+A `[...]` or `[k: v, ...]` case label is parsed as a *structural
+pattern* if and only if at least one of the following holds:
+
+* The literal is empty (`[]` or `[:]`).
+* Some element (or value, in maps) is a *binding form*:
++
+[horizontal]
+`var <ident>` / `def <ident>`:: e.g. `var h` or `def h` (interchangeable)
+`var _` / `def _`:: unnamed binding
+`<Type> <ident>`:: e.g. `Integer h`
+`<Type> _`:: type-checked unnamed binding
+`var... <ident>` / `def... <ident>`:: rest binding
+`... <ident>`:: rest binding (shortcut for `var... <ident>`)
+`<Type>... <ident>`:: typed rest binding
+`var... _` / `def... _`:: rest discard
+`...`:: rest discard (shortcut for `var... _`)
+
+* Some element is a nested pattern (record pattern with at least one
+  unambiguous binding form among its components, nested list pattern,
+  nested map pattern).
+
+Otherwise, the label retains its legacy `isCase` semantics — exactly
+today's behaviour.
+
+The rule is parser-local and does not depend on surrounding scope.
+Every binding form listed above currently fails to parse as a Groovy
+expression in list-literal or map-literal value position, so claiming
+them as pattern grammar does not change the meaning of any program
+valid in Groovy 6.
+
+The bare identifier `_` continues to parse as an identifier in
+expression position — `case [_] ->` therefore retains its legacy
+meaning. Users wanting a single-element wildcard pattern write
+`case [var _] ->`.
+
+=== Bracket-form assignment
+
+In Groovy 7.0, `def [...] = expr` accepts the same pattern grammar as
+switch case labels. This is the bridge between switch and assignment
+destructuring:
+
+[source,groovy]
+----
+def [var h, var... t]                    = list      // canonical
+def [def h, ... t]                       = list      // equivalent (shortcuts)
+def [Integer h, var... t]                = list      // typed head
+def [var first, var... middle, var last] = list      // rest in middle
+def [name: var n, age: var a]            = person    // map destructuring
+def [Point(int x, int y), ... rest]      = list      // nested record pattern
+----
+
+The bracket form supports the full switch pattern grammar — nested
+patterns, type bindings with narrowing, wildcard `_` (via `var _`
+or `def _`), typed rest, and record patterns. A binding marker
+(`var`, `def`, a type, or `...` for rest) is required inside `[...]`
+for the same disambiguation reason it is in case labels: without
+one, the literal would parse as today's legacy list literal.
+
+The parens form (`def (...) = ...`) is a separate, simpler grammar
+covered by GEP-20 and shipped in Groovy 6.x. It remains canonical for
+everyday destructuring (positional bindings, simple rest, map-style
+keys) and does not require `var` markers because the surrounding `def`
+already declares the names. The two forms coexist:
+
+[cols="2,3"]
+|===
+|Form |Capabilities
+
+|Parens (`def (...)`)
+|Positional, rest with `*`, map-style with `key:`. GEP-20, Groovy 6.x.
+
+|Bracket (`def [...]`)
+|Full pattern grammar — nested patterns, wildcard `_`, type narrowing,
+ record patterns. This proposal, Groovy 7.0.
+|===
+
+==== Lowering for bracket-form assignment
+
+The bracket form lowers via the same `Deconstructable` strategy as
+switch case labels, with one accommodation: tail-rest forms accept
+the same RHS contract as GEP-20's parens form (`getAt(IntRange)` or
+`iterator()` fallback), so iterators and unbounded sources work in
+assignment context. List patterns in switch case labels do *not*
+accept iterators — pattern matching against an iterator would
+destructively consume it, which is surprising for a match operation.
+
+The divergence between contexts is therefore:
+
+* `def [var h, var... t] = iter` — accepted, uses `iterator()`
+  fallback, `t` is the iterator (matching GEP-20's parens form).
+* `case [var h, var... t] -> ...` against an iterator — does not
+  match; list patterns require `List`, array, or an `Iterable` that
+  can be materialised non-destructively.
+
+A failed match in a bracket-form declaration throws
+`IllegalArgumentException`. Partial matching is via `switch`.
+
+== Compilation
+
+List and map patterns lower through an internal `Deconstructable`
+strategy that performs:
+
+* a type check (`instanceof List`, `instanceof Map`, `instanceof T[]`,
+  etc.),
+* size or key checks for the named elements,
+* component extraction (`get(int)`, `subList`, `containsKey`/`get`,
+  key-set difference for rest),
+* binding assignment.
+
+Record patterns reuse the lowering already present in 5.0. When Java's
+deconstructor JEP ships, surface forms like
+`case List.of(var h, var... t) ->` and
+`case Map.of("name", var n) ->` are accepted as additional spellings
+that lower to the same `Deconstructable` calls — no re-architecture.
+
+Implementation considerations:
+
+* Switch dispatch on JDK 21+ should evaluate
+  `java.lang.runtime.SwitchBootstraps.typeSwitch` as the dispatch path
+  for arms whose patterns admit it, on parity with how Java compiles
+  pattern switch.
+* Bindings live in synthetic locals; the static type checker propagates
+  narrowed types into them.
+* For dynamic Groovy, bindings are typed `Object` and runtime checks
+  dominate; for `@CompileStatic`, narrowed types let the JIT see
+  through.
+* Iterable inputs that are not `List` or array are materialised as a
+  list once per match attempt; this is observable for iterators with
+  side effects.
+
+== Java alignment
+
+[cols="2,1,2"]
+|===
+|Java feature |Status |Groovy alignment
+
+|Pattern matching for `instanceof` (JEP 394, 16)
+|Shipped
+|Type and record patterns valid in `instanceof`
+
+|Record patterns (JEP 440, 21)
+|Shipped
+|Already in Groovy 5.0; extended in this proposal
+
+|Pattern matching for `switch` (JEP 441, 21)
+|Shipped
+|Arrow-form `switch`, this proposal
+
+|`when` guards (Java 21)
+|Shipped
+|Adopted verbatim
+
+|Unnamed patterns `_` (JEP 456, 22)
+|Shipped
+|Adopted in `Type(...)`; `var _` / `def _` elsewhere (avoids
+ identifier collision); `...` for rest discard (Groovy-native)
+
+|Primitive patterns (JEP 507)
+|Preview
+|Groovy already coerces; revisit when finalised
+
+|Deconstructors for arbitrary classes
+|Draft
+|`Deconstructable` lowering accommodates this when surface syntax is
+ specified
+
+|Array patterns
+|Discussed
+|If Java picks `T[] {...}` syntax, Groovy will accept it as an
+ alternative surface for `case [...]`
+|===
+
+== Excluded and deferred features
+
+[cols="2,1,3"]
+|===
+|Feature |Status |Rationale
+
+|`*name` / `*_` rest sugar (in bracket-form patterns / switch)
+|Deferred
+|The triple-dot shortcut (`...`, `... ident`) supersedes both. `*` is
+GEP-20's canonical rest spelling in `def (...) = ...` (no expression
+collision there) but inside `[...]` patterns `*` collides with
+list-literal spread, so this proposal uses `...` instead. The GEP-20
+spellings (`*ident`, bare `key: ident`) could be accepted inside a
+`[...]` pattern once another binding form elsewhere in the literal
+has triggered pattern mode — they are deferred rather than blocked,
+to keep binding markers locally explicit for readers and to leave
+the design space open for a future relaxation if usage warrants it.
+
+|Or-patterns (`p1 \| p2`)
+|Deferred
+|Existing comma-separated case labels (`case 1, 2, 3 ->`) cover the
+ constant case; pattern alternation is rarely needed and ambiguous with
+ list-element commas
+
+|Type-prefixed list patterns (e.g. `case List<Integer>[var h, var... t]`)
+|Deferred
+|Awaits clarity from Java's array pattern direction; for now, type
+ information flows from the switch input
+
+|Patterns in `for` loops
+|Deferred
+|`for (Map.Entry e in map.entrySet())` is already idiomatic; structural
+ unrolling is a future consideration
+
+|Patterns in `catch` clauses
+|Not planned
+|Multi-catch already covers type unions; structural matching of
+ exception state is rare
+
+|Closed map patterns ("exactly these keys")
+|Not planned
+|Expressible via guard; dedicated syntax not warranted
+
+|Bare `case _ ->` at top level
+|Not planned
+|Conflicts with legacy `_`-as-identifier semantics; users write
+ `default ->` or `case var _ ->`
+
+|Exhaustiveness enforcement
+|Warn-only initially
+|Java errors for sealed-type expression switches; Groovy emits a
+ warning in 7.0 and may escalate via opt-in flag in 7.x
+|===
+
+== Compatibility
+
+=== Backwards compatibility
+
+Every program valid in Groovy 6 compiles with identical semantics in
+Groovy 7. The disambiguation rule is purely parser-local and is
+triggered only by syntactic forms that currently fail to parse:
+
+[cols="3,1"]
+|===
+|Form |Parses today (Groovy 6 with GEP-20)?
+
+|`case [1, 2, 3] ->`                       |Yes — legacy `isCase`, unchanged
+|`case [name: 'x'] ->`                     |Yes — legacy `Map.isCase`, unchanged
+|`case [_] ->`                             |Yes — legacy (list containing `_`)
+|`case [var h, var... t] ->`               |No — new, claims unused grammar
+|`case [def h, def... t] ->`               |No — new (Groovy-native equivalent)
+|`case [... t] ->` / `case [...] ->`       |No — new (`...` shortcut / flipper)
+|`case [Integer h, var... t] ->`           |No — new
+|`case String s ->`                        |No — new (5.0 supported only `Type(...)`)
+|`def [var h, var... t] = list`            |No — new (bracket form)
+|`def [name: var n, age: var a] = person`  |No — new (bracket form)
+|`def (h, *t) = list`                      |Yes — covered by GEP-20 in Groovy 6.x
+|`def (name: n, age: a) = person`          |Yes — covered by GEP-20 in Groovy 6.x
+|===
+
+Two `[...]` forms have legacy semantics that never usefully match —
+`case [] ->` and `case [:] ->` — and are reinterpreted as empty-list
+and empty-map patterns respectively. No existing program depends on
+these never-matching legacy forms.
+
+=== `_` semantics across forms
+
+Wildcard `_` semantics introduced in this proposal apply inside
+`[...]` list and map patterns, and inside `Type(...)` record
+patterns. The parens-form assignment `def (...) = expr` (covered by
+GEP-20) continues to treat `_` as a regular identifier indefinitely,
+matching GEP-20's explicit non-deprecation. Existing idioms such as
+`def (_, y, m) = Calendar.instance` keep compiling unchanged with no
+warning in Groovy 7.
+
+[cols="3,2"]
+|===
+|Context |`_` meaning
+
+|`def (..., _, ...) = expr` (parens form, GEP-20)
+|Identifier — unchanged from today
+
+|`def [var _, ...] = expr` (bracket form)
+|Wildcard — bind-and-discard
+
+|`case [var _, ...] ->` (list pattern in switch)
+|Wildcard — bind-and-discard
+
+|`case Type(_, _) ->` (record pattern)
+|Wildcard — bind-and-discard (Java-aligned)
+|===
+
+This scoping means GEP-19 introduces no behavioural break for any
+existing program: the wildcard semantics live in grammar (`[...]`
+patterns, `Type(...)` patterns) that does not parse today.
+
+== References
+
+* https://openjdk.org/jeps/394[JEP 394: Pattern Matching for instanceof]
+* https://openjdk.org/jeps/440[JEP 440: Record Patterns]
+* https://openjdk.org/jeps/441[JEP 441: Pattern Matching for switch]
+* https://openjdk.org/jeps/456[JEP 456: Unnamed Variables & Patterns]
+* https://openjdk.org/jeps/507[JEP 507: Primitive Types in Patterns]
+* https://docs.python.org/3/whatsnew/3.10.html#pep-634-structural-pattern-matching[PEP 634: Structural Pattern Matching (Python)]
+* link:GEP-18.html[GEP-18: Integrated Concurrency and Parallel Processing]
+* link:GEP-20.html[GEP-20: Multi-assignment Destructuring]


(groovy-website) 01/02: draft GEP for additional multi-assignment forms (minor tweaks)

Posted by pa...@apache.org.
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 5fbacb66a0add8b4c7c10810731c9bb27c29cc1b
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Apr 26 06:42:15 2026 +1000

    draft GEP for additional multi-assignment forms (minor tweaks)
---
 site/src/site/wiki/GEP-20.adoc | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/site/src/site/wiki/GEP-20.adoc b/site/src/site/wiki/GEP-20.adoc
index dab0082..d04b0b2 100644
--- a/site/src/site/wiki/GEP-20.adoc
+++ b/site/src/site/wiki/GEP-20.adoc
@@ -56,7 +56,7 @@ bracket form, no marker words) while filling the gaps.
 
 === Design principles
 
-* *Strictly additive* — every program valid in Groovy 5 / 6 compiles
+* *Strictly additive* — every program valid in Groovy 4 / 5 compiles
   with identical semantics. New forms use grammar that fails to parse
   today.
 
@@ -485,7 +485,7 @@ GEP-19 pattern matching); others are not planned.
  later without grammar collision
 
 |Per-binder modifiers (e.g. `def (final a, b) = …`)
-|Not planned
+|Deferred
 |Modifiers belong on the outer declaration: `final (a, b) = [1, 2]`
  makes both binders final and propagates to rest and map-style
  binders too. Per-binder modifiers would only enable mixed-modifier