Skip to content

Reference implementation: structural pattern matching for switch/instanceof (GEP-19 Groovy 7) - #2655

Closed
paulk-asert wants to merge 1 commit into
apache:masterfrom
paulk-asert:worktree-gep19
Closed

paulk-asert wants to merge 1 commit 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, now rebased onto the Groovy 7 master (first-class switch expressions, GROOVY-12255), so we have 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.

Lowering: every pattern arm is an ordinary CaseStatement of the first-class
SwitchExpression: its label is a boolean test on a synthetic subject variable (bound
to the evaluated selector by the bytecode writers instead of a temp slot), and its code
starts with the matching steps, each if (!(check)) break <arm>, where the arm's
statement label is registered by the writers as the next case test. Pattern variables
come from instanceof bindings and JEP 394 flow scoping, so there are no closures,
casts or per-arm allocations, and records/lists/maps are deconstructed once per match.
Under @CompileStatic the arms are plain INSTANCEOF branches.

Prerequisite fix (first commit, standalone): a pattern variable declared in a later
operand of &&/|| had its slot initialised only at its own instanceof site, so
p instanceof String s && s.length() > 0 && s.trim() instanceof String t failed
verification on master; the slots are now pre-declared. The same commit makes an arm
ending in break/continue count as not completing normally for pattern-variable
flow scoping (JLS §14.22).

Compatibility

Everything is parse-time lowering to ordinary AST plus node metadata
(org.apache.groovy.ast.tools.SwitchPatternUtils) — 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.

Like any other switch expression under type checking, a non-exhaustive pattern switch is
a compile error (the earlier version of this PR issued a warning); an unconditional
pattern or coverage of a sealed hierarchy makes it exhaustive.

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).
  • SwitchBootstraps.typeSwitch indy dispatch (needs a Java 21+ bytecode target).
    Primitive type patterns (JEP 507) are included, ahead of the GEP text.

@paulk-asert
paulk-asert marked this pull request as draft July 2, 2026 13:43
@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
@paulk-asert paulk-asert changed the title Worktree gep19 Reference implementation: structural pattern matching for switch/instanceof (GEP-19) Jul 7, 2026
@paulk-asert paulk-asert changed the title Reference implementation: structural pattern matching for switch/instanceof (GEP-19) Reference implementation: structural pattern matching for switch/instanceof (GEP-19 Groovy 7) Jul 17, 2026
@paulk-asert

Copy link
Copy Markdown
Contributor Author

Rebased onto the Groovy 7 master as two commits (the nine phase commits are squashed into the second):

  1. GROOVY-12242 follow-up: pattern variable slots bound in a later &&/|| operand were only initialised at their own instanceof site, which fails verification on master for e.g. p instanceof String s && s.length() > 0 && s.trim() instanceof String t; and break/continue now end an if arm for pattern-variable flow scoping. Standalone and cherry-pickable.
  2. The GEP-19 implementation re-lowered onto the first-class SwitchExpression of GROOVY-12255: no closure labels or closure-free fast path any more; each pattern arm is a CaseStatement with a boolean test label and if (!(check)) break <arm> matching steps, bindings via JEP 394 flow scoping (see the updated description). Exhaustiveness follows the existing switch-expression rule (error, not warning). The fail/SwitchExpression_11x..15x resources are now 15x..19x.

Verified with the full core test suite and the lint gate.

…ce implementation)

Ports PR apache#2655 onto the first-class switch expression AST (GROOVY-12255).
Everything is parse-time lowering to ordinary AST plus node metadata
(org.apache.groovy.ast.tools.SwitchPatternUtils):

- a pattern switch carries a synthetic subject Parameter; VariableScopeVisitor
  declares it, the type checker types it like a loop variable and the
  bytecode writers bind it to the evaluated selector instead of a temp slot
- a pattern arm is a CaseStatement marked PATTERN_ARM whose label is a boolean
  test on the subject (the writers evaluate it instead of calling isCase) and
  whose code starts with the matching steps, each `if (!(check)) break <arm>`
  with the arm's statement label registered as the next case test; pattern
  variables come from instanceof bindings and JEP 394 flow scoping, so no
  casts, closures or per-arm allocation remain (the closure-label and
  "closure-free" lowerings of the PR are gone)
- type patterns with `when` guards, record patterns (native, emulated, Java
  records and toList() deconstructables via RecordPatternSupport), list
  patterns (ListPatternSupport) and map patterns (MapPatternSupport) in case
  labels; record patterns in instanceof as a short-circuit && chain of
  bindings, a var component bound through RecordPatternSupport.bindable/bound
  so that a null component still matches
- static type checking: an error for a pattern that cannot match the subject
  type or a record pattern of the wrong arity, a warning for a dominated
  pattern; an unconditional pattern or sealed-hierarchy coverage makes the
  switch exhaustive, otherwise the existing "does not cover all possible
  input values" error applies (the PR issued a warning)
- primitive type patterns (JEP 507 alignment) as in the PR's last commit

Test resources fail/SwitchExpression_11x..15x of the PR are 15x..19x here.
@testlens-app

testlens-app Bot commented Sep 10, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 7b0e77e
▶️ Tests: 118496 executed
⚪️ Checks: 25/25 completed


Learn more about TestLens at testlens.app/docs.

@paulk-asert

Copy link
Copy Markdown
Contributor Author

superseded by #2933

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