You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "Paul King (Jira)" <ji...@apache.org> on 2026/08/20 04:45:00 UTC

[jira] [Resolved] (GROOVY-12255) Compile switch expressions as first-class AST (no closure desugar)

     [ https://issues.apache.org/jira/browse/GROOVY-12255?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Paul King resolved GROOVY-12255.
--------------------------------
    Fix Version/s: 6.0.0-beta-3
       Resolution: Fixed

> Compile switch expressions as first-class AST (no closure desugar)
> ------------------------------------------------------------------
>
>                 Key: GROOVY-12255
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12255
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Assignee: Daniel Sun
>            Priority: Major
>              Labels: breaking
>             Fix For: 6.0.0-beta-3
>
>
> h3. Problem
> GROOVY-9272 added switch expressions. The 4.0 implementation rewrites them in {{AstBuilder}} to an immediately-called closure around a switch {{{}statement{}}}:
> {code:groovy}
> // source
> def r = switch (x) {
>     case 0, 1 -> 'a'
>     default   -> 'z'
> }
> // compiled as
> def r = { ->
>     switch (x) {
>         case 0:
>         case 1:  return 'a'
>         default: return 'z'
>     }
> }.call()
> {code}
> That is a simulation, not a JEP 361 switch expression:
>  * every evaluation allocates a closure and an extra call frame
>  * an unmatched selector completes with {{null}} instead of throwing
>  * {{return}} / {{break}} / {{continue}} are interpreted against the synthetic closure, not the enclosing method
>  * locals assigned in an arm are closure-shared, not method locals
>  * {{@CompileStatic}} cannot emit {{tableswitch}} / {{lookupswitch}} the way javac does
> h3. Goal
> Compile a switch expression as a first-class {{SwitchExpression}} whose arms {{yield}} (or throw). Emit the result on the operand stack. Keep Groovy {{isCase}} matching (Class, regex, Collection, Closure). Align control flow and exhaustiveness with [JEP 361|https://openjdk.org/jeps/361] for both dynamic Groovy and {{@TypeChecked}} / {{{}@CompileStatic{}}}.
> h3. Proposed shape
>  * Parser builds {{SwitchExpression}} / {{{}YieldStatement{}}}; arrow expressions become implicit {{{}yield{}}}. No closure wrapper.
>  * Codegen: join all completing arms at one label with the value on the stack. When the selector and labels allow it, emit {{tableswitch}} / {{{}lookupswitch{}}}, the Java string-switch (hash + {{equals}} + second switch), or {{{}Enum.ordinal(){}}}; otherwise sequential {{{}isCase{}}}.
>  * Exhaustiveness: unmatched dynamic selector throws {{{}IllegalStateException{}}}; a complete enum may omit {{default}} (synthetic {{IncompatibleClassChangeError}} if a new constant appears at runtime). {{@TypeChecked}} / {{@CompileStatic}} reject a provably non-exhaustive expression at compile time.
>  * Control flow: {{return}} must not leave the enclosing method through a switch expression; {{yield}} must not jump through a nested closure/lambda. An arrow arm must {{yield}} or throw on every path.
> {code:groovy}
> int n = switch (day) {
>     case MONDAY, FRIDAY -> 6
>     case TUESDAY        -> 7
>     default             -> {
>         int len = day.toString().length()
>         yield len
>     }
> }
> {code}
> h3. Compatibility
> ||topic||4.0-5.x (closure rewrite)||after this change||
> |unmatched selector (dynamic)|{{null}}|{{IllegalStateException}}|
> |non-exhaustive under STC / CS|often accepted|compile error (unless a complete enum)|
> |arrow block with no {{yield}}|last expression is the closure result|compile error unless every path yields or throws|
> |Groovy {{isCase}} cases|works|still works (fast path only when labels are int / String / enum constants)|



--
This message was sent by Atlassian Jira
(v8.20.10#820010)