Reference implementation: structural pattern matching for switch/instanceof (GEP-19 Groovy 7) - #2655
Closed
paulk-asert wants to merge 1 commit into
Closed
paulk-asert wants to merge 1 commit into
paulk-asert wants to merge 1 commit into
Conversation
paulk-asert
marked this pull request as draft
July 2, 2026 13:43
paulk-asert
marked this pull request as ready for review
July 2, 2026 21:41
paulk-asert
marked this pull request as draft
July 2, 2026 21:41
Contributor
There was a problem hiding this comment.
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
AstBuilderto 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 integrateinstanceofrecord-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
force-pushed
the
worktree-gep19
branch
from
July 17, 2026 04:21
5fcba6e to
56e6c2c
Compare
paulk-asert
force-pushed
the
worktree-gep19
branch
from
September 10, 2026 12:04
56e6c2c to
1e11bbf
Compare
Contributor
Author
|
Rebased onto the Groovy 7 master as two commits (the nine phase commits are squashed into the second):
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.
paulk-asert
force-pushed
the
worktree-gep19
branch
from
September 10, 2026 22:05
1e11bbf to
7b0e77e
Compare
✅ All tests passed ✅🏷️ Commit: 7b0e77e Learn more about TestLens at testlens.app/docs. |
Contributor
Author
|
superseded by #2933 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
switchandinstanceofImplements 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
whenguards (switch expressions / arrow-form switch, aligns with JEP 394/441):Record patterns in
caselabels andinstanceof(aligns with JEP 440), positional, withnesting,
var/defbindings and the_wildcard. Works for native and emulated Groovyrecords, Java records, and any class providing
toList()(deconstruction goes through thenew
RecordPatternSupportruntime, resolving components via the MOP):List patterns — literals, typed elements, nested patterns, and a single rest binding
(
var... t,Integer... t,... t, bare...) in any position; destructureList,arrays and re-iterable
Iterables (ListPatternSupport):Map patterns — open matching (named keys must be present, extras ignored), literal
values by equality, nested patterns as values,
... restbinding for the remainingentries,
[:]for the empty map; keys must be constants (MapPatternSupport):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/
@CompileStaticnarrowing.Lowering: every pattern arm is an ordinary
CaseStatementof the first-classSwitchExpression: its label is a boolean test on a synthetic subject variable (boundto 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'sstatement label is registered by the writers as the next case test. Pattern variables
come from
instanceofbindings 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
@CompileStaticthe arms are plainINSTANCEOFbranches.Prerequisite fix (first commit, standalone): a pattern variable declared in a later
operand of
&&/||had its slot initialised only at its owninstanceofsite, sop instanceof String s && s.length() > 0 && s.trim() instanceof String tfailedverification on master; the slots are now pre-declared. The same commit makes an arm
ending in
break/continuecount as not completing normally for pattern-variableflow 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 formatchanges. Pattern labels require the arrow form. Legacy
isCaselabels are fully preservedand 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 itis empty or contains a binding form / rest / nested pattern. Known carve-outs (e.g.
case List<String> lnow 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
def [...] = expr(GEP phase for Groovy 7) — deferred forspec-level review (overlap with the shipped GEP-20 parens form; no Java anchor yet).
instanceof(no type at the head, per the GEP).SwitchBootstraps.typeSwitchindy dispatch (needs a Java 21+ bytecode target).Primitive type patterns (JEP 507) are included, ahead of the GEP text.