Reference implementation: structural pattern matching for switch/instanceof (GEP-19 Groovy 7 v2) - #2933
Open
paulk-asert wants to merge 3 commits into
Open
Reference implementation: structural pattern matching for switch/instanceof (GEP-19 Groovy 7 v2)#2933paulk-asert wants to merge 3 commits into
paulk-asert wants to merge 3 commits into
Conversation
…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.
… syntax tests Follow-up to the port, applying what the switch-position work established. A switch statement carrying a pattern label is now checked for exhaustiveness. Until GROOVY-12408 that happened by accident: GROOVY-12255 made every arrow switch an expression, so a pattern switch written as a statement, or as a whole method body, went down the expression path and was checked there. Now that position decides, such a switch really is a statement and nothing was checking it. The check is triggered by a pattern label and nothing else. That matters for compatibility, because the obvious wiring is to reuse the expression check, which is unconditional by design: every switch expression must be exhaustive whatever its labels. Reusing it would demand a default from every switch statement in existence. JEP 441 draws the line in the same place, requiring exhaustiveness of a statement only when it uses a pattern label; constant and class-literal labels never demand a default. Pattern syntax is new here, so no existing source can contain one and none is affected. A class literal can still help satisfy the check once a pattern has triggered it, which keeps what triggers the requirement separate from what satisfies it. Whether to follow Java in treating `case null` as a trigger is left open. Java has that rule because `case null` is itself new in Java 21; in Groovy it is long-standing and works today, so adopting the trigger would break working code and is a decision for the GEP rather than a detail here. checkSwitchExpressionExhaustiveness and coversAllEnumConstants took a SwitchExpression but needed only the selector, the case list and the default, so both are generalised the way GROOVY-12406 generalised the duplicate-label check. The statement diagnostic reads "the switch statement does not cover all possible input values", matching javac. The ten parse-and-run resource tests hung off GroovyParserTest and SyntaxErrorTest, which GROOVY-12400 replaced with the generated covering-set tests. They now have their own driver: the covering set exercises the grammar, these exercise the feature.
The GEP uses `var... rest` as the canonical spelling for a map pattern's rest binding, in the abstract, in the design principles and in the map pattern section. Only the bare `... rest` shortcut parsed; `var... rest` failed with "Missing ':'". The list form already allowed the prefix, through listPatternRest's `(DEF | VAR | standardType)?`, but mapPatternEntry's rest alternative was a bare `ELLIPSIS identifier?`. It now takes the same `var` / `def` prefix. No type prefix, unlike the list form: a map pattern's rest binding is always a Map, so `Integer... rest` has nothing to test and stays an error. The AST builder needs no change, since it reads ELLIPSIS and identifier from the entry either way. Legacy map labels are unaffected, verified against master: `case [a: 1]` still uses Map.isCase and `case [1, 2, 3]` still tests containment.
✅ All tests passed ✅🏷️ Commit: f246e46 Learn more about TestLens at testlens.app/docs. |
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 yet. This is the reference implementation for GEP-19, re-ported onto current master so we have something concrete to discuss for the feature boundary and for performance testing. It replaces #2655, which was based on a master from before the Groovy 6 switch work and no longer applied.
What changed since #2655
switchproduces a value, and switch statements have the same jump-table dispatch and duplicate-label diagnostic as switch expressions.var... restnow parses in a map pattern. Only the bare... restshortcut worked, though the GEP usesvar... restas its canonical spelling.def [...] = expris deferred and is not implemented here. The GEP has been updated to match.What's included
Type patterns with
whenguards, aligning with JEP 394/441:Record patterns in
caselabels and ininstanceof(JEP 440), positional, nestable, withvar/defbindings and the_wildcard. Works for native and emulated Groovy records, Java records, and any class providingtoList():List patterns — literals, typed elements, nested patterns, and a single rest binding in any position, over
List, arrays and re-iterableIterables:Map patterns — open matching, literal values by equality, nested patterns as values, and a rest binding for the remaining entries:
Exhaustiveness, and why the trigger matters
A switch expression must always be exhaustive; that is GROOVY-12255 and is unchanged here. A switch statement is now checked too, but only when it carries a pattern label, which is where JEP 441 draws the line.
The gating is the important part. The obvious implementation is to reuse the check the expression path already calls, but that check is unconditional by design, so reusing it would demand a
defaultfrom every switch statement in existence. Keying on pattern labels means no existing source is affected at all, since pattern syntax is new and no released Groovy parses it. A class-literal label can still help satisfy the check once a pattern has triggered it, which keeps what triggers the requirement separate from what satisfies it.Whether
case nullshould also trigger it, as in Java, is left open. Java has that rule becausecase nullis itself new in Java 21; in Groovy it is long-standing and matches today, so adopting it would reject working code.Lowering
Everything is parse-time desugaring: no new AST node kinds and no bytecode-format changes. Each pattern arm is an ordinary
CaseStatementwhose label is a boolean test on a synthetic subject variable, with the matching steps asif (!check) break <arm>. Pattern variables come frominstanceofbindings and JEP 394 flow scoping, so there are no closures, casts or per-arm allocations, and a value is destructured once per match. Under@CompileStaticthe arms are plainINSTANCEOFbranches.Compatibility
Legacy
isCasematching is untouched and mixes freely with patterns in one switch:case foo(bar),case [1, 2, 3](containment) andcase [a: true](lookup) keep today's semantics. Pattern mode is opt-in through binding markers.Two carve-outs, both itemised in the GEP:
case List<String> l ->was a chained comparison that could never usefully evaluate; it is now a type pattern.case Foo(_) ->, whereFoonames a method rather than a type and_is in scope as a variable, was a call label and is now a record pattern. Only the capitalised spelling is affected;case foo(_) ->still callsfoo.Colon-form labels are deliberately unaffected: a pattern requires the arrow form, so a colon label is never read as one and keeps whatever it means today.
Deliberately not included
def [...] = expr— deferred pending review. It overlaps the shipped GEP-20 parens form, forks rest semantics between switch and assignment, and has the opposite failure philosophy (strict vs null-padding).instanceof— no type at the head, per the GEP.SwitchBootstraps.typeSwitchindy dispatch — needs a Java 21+ bytecode target.Open for discussion
case nullas an exhaustiveness trigger.return/break/continuein arrow arms. Rejected since the 4.0 parser and legal in Java. Pattern labels are arrow-only, so under this proposal there is no workaround; worth fixing alongside.MatchExceptionand noSwitchBootstraps.typeSwitch.