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

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

Daniel Sun created GROOVY-12255:
-----------------------------------

             Summary: 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: Dependency upgrade
            Reporter: Daniel Sun


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-6.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)