Worktree gep19#2655
Draft
paulk-asert wants to merge 9 commits into
Draft
Conversation
… 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)
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)
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 |
…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)
✅ All tests passed ✅🏷️ Commit: 5fcba6e Learn more about TestLens at testlens.app. |
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. 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
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.Performance: an all-pattern switch lowers to closure-free nested-
instanceofarms —measured ~6x faster under
@CompileStatic(arms become plainINSTANCEOFbranches) 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
isCaselabels are fully preserved and canmix 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.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).