Skip to content

Worktree gep19#2655

Draft
paulk-asert wants to merge 9 commits into
apache:masterfrom
paulk-asert:worktree-gep19
Draft

Worktree gep19#2655
paulk-asert wants to merge 9 commits into
apache:masterfrom
paulk-asert:worktree-gep19

Conversation

@paulk-asert

@paulk-asert paulk-asert commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

NOT for MERGING just yet! This is a reference implementation for GEP-19. I think we still want to target Groovy 7 and we're not quite ready to branch for that yet, but this gives us something to discuss about the feature boundary for what we deliver and for performance testing.

GEP-19: structural pattern matching in switch and instanceof

Implements the core of GEP-19
(see the GEP for full syntax and semantics; this is a reviewer's summary).

What's included

Type patterns with when guards (switch expressions / arrow-form switch, aligns with JEP 394/441):

def desc = switch (obj) {
    case Integer i when i > 0 -> "positive $i"
    case String s             -> s.toUpperCase()
    default                   -> 'other'
}

Record patterns in case labels and instanceof (aligns with JEP 440), positional, with
nesting, var/def bindings and the _ wildcard. Works for native and emulated Groovy
records, Java records, and any class providing toList() (deconstruction goes through the
new RecordPatternSupport runtime, resolving components via the MOP):

case Line(Point(_, var y), Point p2) -> ...
if (p instanceof Point(int x, int y)) { ... }      // any expression context

List patterns — literals, typed elements, nested patterns, and a single rest binding
(var... t, Integer... t, ... t, bare ...) in any position; destructure List,
arrays and re-iterable Iterables (ListPatternSupport):

case []                                   -> 'empty'
case [1, var x, ...]                      -> "starts with 1, then $x"
case [var first, var... middle, var last] -> ...

Map patterns — open matching (named keys must be present, extras ignored), literal
values by equality, nested patterns as values, ... rest binding for the remaining
entries, [:] for the empty map; keys must be constants (MapPatternSupport):

case [type: 'circle', radius: var r] -> "circle r=$r"
case [name: String n, ... rest]      -> "named $n; others=$rest"

Static type checking (STC only — dynamic Groovy stays permissive): errors for patterns
that provably cannot match the subject type and for record-pattern arity mismatches;
warnings for dominated (unreachable) arms and non-exhaustive pattern switches (default /
unconditional pattern / sealed-hierarchy coverage all recognized). Pattern variables get
full STC/@CompileStatic narrowing.

Performance: an all-pattern switch lowers to closure-free nested-instanceof arms —
measured ~6x faster under @CompileStatic (arms become plain INSTANCEOF branches) and
~1.4x faster in dynamic code versus the closure-label lowering, with records/lists/maps
deconstructed once per match.

Compatibility

Everything is parse-time desugaring — no new AST nodes, no bytecode format changes.
Pattern labels require the arrow form. Legacy isCase labels are fully preserved and can
mix with patterns in one switch: case foo(bar), case [1, 2, 3] (containment),
case [a: true] (lookup) keep today's semantics — a [...] label is only a pattern if it
is empty or contains a binding form / rest / nested pattern. Known carve-outs (e.g.
case List<String> l now parses as a pattern) are flagged for the GEP doc.

Deliberately not included

  • Bracket-form assignment def [...] = expr (GEP phase for Groovy 7) — deferred for
    spec-level review (overlap with the shipped GEP-20 parens form; no Java anchor yet).
  • List/map patterns in instanceof (no type at the head, per the GEP).
  • Primitive type patterns (JEP 507 is still preview) and SwitchBootstraps.typeSwitch
    indy dispatch (needs a Java 21+ bytecode target).

… with binding and when guards)

Adds arrow-form case labels of the form `case Type ident ->` and
`case Type ident when guard ->` to switch statements and expressions.
Patterns are lowered in the parser: the switch subject becomes a closure
parameter, an unguarded pattern becomes a class literal label, a guarded
pattern becomes a closure label testing the type then the guard with the
pattern variable in scope, and each pattern case body is prefixed with a
declaration of the narrowed pattern variable. Static type checking and
static compilation work unchanged; legacy isCase labels mix freely with
patterns. Pattern labels require the arrow form and primitive type
patterns are rejected while JEP 507 remains in preview.

Assisted-by: Claude Code (Claude Fable 5)
…eness, dominance and compatibility checks)

The parser records the original subject expression and per-label pattern
type/guard metadata on the lowered switch. The static type checker uses
the recorded subject for the switch condition type, restoring unqualified
enum-constant labels and closure-label parameter hints in switches that
also contain patterns, and checks pattern labels: an error for a pattern
type provably disjoint from the subject type, a warning for an arm
dominated by a preceding type test, and a warning for a non-exhaustive
pattern switch (no default, no unconditional pattern, sealed hierarchy
not fully covered). Exhaustiveness is only assessed when every label is
statically analysable. Dynamic Groovy is unaffected.

Assisted-by: Claude Code (Claude Fable 5)
@paulk-asert paulk-asert marked this pull request as draft July 2, 2026 13:43
Adds record patterns to arrow-form case labels, e.g.
`case Point(int x, int y) when x == y ->`, with nested record patterns,
`_` wildcards, and var/def component bindings. Record patterns are
positional and component names are unknown at parse time, so the
lowering emits calls to the new RecordPatternSupport runtime helper,
which deconstructs native records through their record components and
falls back to toList() for other deconstructable values such as
emulated Groovy records. A closure case label performs the type, arity
and component checks; component bindings are redeclared in the case
body. Components are mandatory and must be pattern-shaped so legacy
method call labels such as `case foo()` and `case foo(bar)` keep
isCase semantics. The static type checker reports an arity mismatch
against the record's components, treats record patterns as conditional
for dominance purposes, and leaves exhaustiveness unassessed for
switches containing them.

Assisted-by: Claude Code (Claude Fable 5)
…ns in instanceof)

Extends instanceof to accept record patterns, e.g.
`if (p instanceof Point(int x, var y))`, including nested record
patterns, `_` wildcards and var/def component bindings, usable in any
expression context (if, while, ternary, boolean composition) under
both dynamic and static compilation. The lowering emits a conjunction
of ordinary expressions and instanceof bindings (JEP 394), combined
with the non-short-circuiting `&` operator: an instanceof binding
declares and default-initialises its variable where it occurs, so
evaluating every conjunct unconditionally keeps all pattern variables
definitely assigned for the bytecode verifier (short-circuit joins
merge frames in which the variables do not yet exist). New
RecordPatternSupport helpers keep each conjunct safe to evaluate after
an earlier conjunct has failed while never invoking accessors on
values that did not pass their own type check. A var/def component is
combined with `| true`, giving it unconditional semantics: a null
component keeps the match alive and leaves the binding null.

Assisted-by: Claude Code (Claude Fable 5)
List patterns destructure List, array and Iterable values in switch case
labels: element positions accept literals (matched by equality), type
patterns, var/def bindings, nested record/list patterns and a single rest
binding in any position (var... t, Type... t, ... t, or bare ...). A [...]
literal is a pattern iff it is empty or some element is a binding form or
nested pattern; otherwise it keeps its legacy isCase (containment)
semantics, including the colon form. Destructuring goes through the new
ListPatternSupport runtime helper. The static type checker rejects a list
pattern whose subject provably cannot be a List, array or Iterable, and
treats list patterns as conditional for exhaustiveness. Not valid in
instanceof (no type at the head), per the GEP.

Assisted-by: Claude Code (Claude Fable 5)
Map patterns destructure Map values in switch case labels with open
semantics: a pattern matches if all named keys are present and their value
patterns match; extra entries are ignored unless captured by a rest binding
(... rest binds the remaining entries as a map copy, bare ... discards
them). Entry values accept literals (matched by equality), type patterns,
var/def bindings and nested record/list/map patterns; map patterns also
nest within list patterns. Keys must be constants. [:] is the empty map
pattern. A [k: v, ...] literal without a binding form, rest or nested
pattern keeps its legacy isCase (boolean lookup) semantics, including the
colon form. Destructuring goes through the new MapPatternSupport runtime
helper. The static type checker rejects a map pattern whose subject
provably cannot be a Map and treats map patterns as conditional for
exhaustiveness. Not valid in instanceof (no type at the head), per the GEP.

Assisted-by: Claude Code (Claude Fable 5)
…lowering)

A switch expression whose case labels are all patterns now lowers each arm
to nested if statements inside its own block instead of closure case
labels: check steps become guarding ifs and binding steps (including the
JEP 394 instanceof bindings) execute between them, with the when guard as
the innermost check. Statement nesting short-circuits and every binding
declaration dominates its reads, per-arm scoping lets pattern variable
names repeat across arms, no per-arm closure is allocated or dispatched
through Closure.isCase, and record/list/map patterns are destructured once
per match rather than twice (pinned by a new test). Measured on a mixed
five-way dispatch (1M iterations): ~1.4x faster dynamic, ~6x faster under
@CompileStatic, where arms compile to plain INSTANCEOF branches. Switches
mixing patterns with legacy labels keep the closure-label lowering. The
static type checker runs the same pattern label checks over the arm
metadata now attached to the generated closure (which, unlike the call
expression, survives ResolveVisitor's rebuild). Emitting
SwitchBootstraps.typeSwitch indy dispatch remains future work for when a
Java 21+ bytecode target is available; JEP 507 primitive patterns remain
deferred.

Assisted-by: Claude Code (Claude Fable 5)
@paulk-asert paulk-asert marked this pull request as ready for review July 2, 2026 21:41
@paulk-asert paulk-asert marked this pull request as draft July 2, 2026 21:41
@paulk-asert paulk-asert requested a review from Copilot July 3, 2026 04:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reference implementation of GEP-19 structural pattern matching for Groovy switch expressions (arrow-form) and instanceof, including parse-time desugaring/lowering, runtime deconstruction helpers, and static type-checking (STC) diagnostics.

Changes:

  • Extend the ANTLR grammar and AstBuilder to parse pattern forms (type, record, list, map) and lower pattern switches (including a closure-free fast path for all-pattern switches).
  • Add runtime support utilities for deconstruction/matching (RecordPatternSupport, ListPatternSupport, MapPatternSupport) and integrate instanceof record-pattern lowering.
  • Add STC validation for pattern switches (incompatible patterns, dominated arms, limited exhaustiveness warnings) plus extensive parser + semantic tests and resources.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy Registers new negative parser cases for pattern-switch failures.
src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy Registers new positive parser fixtures for pattern switch expressions.
src/test/groovy/groovy/SwitchPatternMatchingTest.groovy New end-to-end tests for switch pattern matching semantics + STC warnings.
src/test/groovy/groovy/InstanceofTest.groovy Adds tests for record patterns in instanceof across contexts.
src/test-resources/fail/SwitchExpression_11x.groovy Negative fixture: pattern labels require arrow form.
src/test-resources/fail/SwitchExpression_12x.groovy Negative fixture: primitive type patterns not supported.
src/test-resources/fail/SwitchExpression_13x.groovy Negative fixture: record pattern case labels require arrow form.
src/test-resources/fail/SwitchExpression_14x.groovy Negative fixture: list pattern allows at most one rest binding.
src/test-resources/fail/SwitchExpression_15x.groovy Negative fixture: map pattern keys must be constants.
src/test-resources/core/SwitchExpression_27x.groovy Positive fixture: type patterns + when guards + mixed legacy labels.
src/test-resources/core/SwitchExpression_28x.groovy Positive fixture: record patterns in switch expressions.
src/test-resources/core/SwitchExpression_29x.groovy Positive fixture: list patterns in switch expressions.
src/test-resources/core/SwitchExpression_30x.groovy Positive fixture: map patterns in switch expressions.
src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java Adds STC analysis for pattern switch arms (errors + warnings).
src/main/java/org/apache/groovy/runtime/RecordPatternSupport.java New runtime helper for record/toList-based deconstruction used by lowering.
src/main/java/org/apache/groovy/runtime/ListPatternSupport.java New runtime helper for list/array/iterable materialization and rest handling.
src/main/java/org/apache/groovy/runtime/MapPatternSupport.java New runtime helper for map entry access and rest-binding extraction.
src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java Core parsing + lowering for GEP-19 patterns in switch and instanceof.
src/antlr/GroovyParser.g4 Grammar extensions for case patterns, record patterns, list/map patterns, and guards.

Comment on lines +4912 to +4915
int componentCount = patternType.getRecordComponents().size();
if (componentCount > 0 && componentCount != recordPatternArity) {
addStaticTypeError("The record pattern specifies " + recordPatternArity + " component(s) but " + prettyPrintTypeName(patternType) + " has " + componentCount, label);
}
Comment on lines +1605 to +1611
} else if (element.type != null) {
ClassNode bindType = ClassHelper.getWrapper(element.type);
Expression rhs = element.name != null
? declX(varX(element.name, bindType), EmptyExpression.INSTANCE)
: new ClassExpression(bindType);
items.add(binX(access, instanceOf, rhs));
} else { // var/def binding: unconditional, also matches a null value
…component binding types)

The STC record pattern arity check now also fires for zero-component
records: any record pattern against one is a statically-known mismatch
(a pattern has at least one component). isRecord() covers native records
with no components; a positive component count keeps covering emulated
records; other toList() deconstructables remain unchecked.

The closure-free switch lowering now binds a primitive-typed component
with its declared primitive type (the instanceof check still uses the
wrapper, via a synthetic local), matching the closure-label lowering and
Java record patterns (JEP 440), so a pattern variable's static type no
longer depends on which lowering a switch takes. Both behaviours are
pinned by new tests.

Assisted-by: Claude Code (Claude Fable 5)
Groovy 7 timeframes are expected to align with JEP 507 becoming stable,
so `case int i` is now accepted rather than rejected. Following JEP 507
semantics for a reference-typed subject, a primitive type pattern tests
the wrapper type (an Integer matches `int` but a Byte or Long does not;
no widening or narrowing) and binds the pattern variable with its
declared primitive type, in both the closure-label and closure-free
lowerings. A record pattern with a primitive type (e.g. `case int(var x)`)
remains an error, now with a message saying why. The feature is
incubating, so semantics can be tweaked if JEP 507 shifts before it
finalises.

Assisted-by: Claude Code (Claude Fable 5)
@testlens-app

testlens-app Bot commented Jul 4, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 5fcba6e
▶️ Tests: 68501 executed
⚪️ Checks: 23/23 completed


Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants