[ 
https://issues.apache.org/jira/browse/GROOVY-12255?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18104852#comment-18104852
 ] 

ASF GitHub Bot commented on GROOVY-12255:
-----------------------------------------

daniellansun commented on code in PR #2784:
URL: https://github.com/apache/groovy/pull/2784#discussion_r3785713010


##########
src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy:
##########
@@ -0,0 +1,670 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.classgen
+
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.ExpressionTransformer
+import org.codehaus.groovy.ast.expr.SwitchExpression
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * GROOVY-12255: first-class switch expressions (JEP 361) for dynamic and 
static Groovy.
+ * Compiles as {@code SwitchExpression} / {@code YieldStatement}, not as a
+ * closure wrapping a switch statement.
+ */
+final class Groovy12255 {
+
+    @Test
+    void arrowExpressionArms() {
+        assertScript '''
+            def letter = switch (2) {
+                case 1 -> 'a'
+                case 2 -> 'b'
+                default -> 'z'
+            }
+            assert letter == 'b'
+        '''
+    }
+
+    @Test
+    void commaSeparatedArrowLabels() {
+        assertScript '''
+            def n = switch (8) {
+                case 6, 8, 10 -> 3
+                default -> 0
+            }
+            assert n == 3
+        '''
+    }
+
+    @Test
+    void yieldInArrowBlock() {
+        assertScript '''
+            def n = switch (2) {
+                case 1 -> 10
+                case 2 -> {
+                    int doubled = 2 * 10
+                    yield doubled
+                }
+                default -> 0
+            }
+            assert n == 20
+        '''
+    }
+
+    @Test
+    void colonStyleWithYieldAndFallThrough() {
+        assertScript '''
+            def s = 'Bar'
+            int result = switch (s) {
+                case 'Foo':
+                    yield 1
+                case 'Bar':
+                    // fall through
+                case 'Baz':
+                    yield 2
+                default:
+                    yield 0
+            }
+            assert result == 2
+        '''
+    }
+
+    @Test
+    void throwFromArm() {
+        def err = shouldFail(RuntimeException, '''
+            def x = 9
+            def r = switch (x) {
+                case 1 -> 1
+                default -> throw new RuntimeException('nope')
+            }
+        ''')
+        assert err.message == 'nope'
+    }
+
+    @Test
+    void unmatchedSelectorThrows() {
+        def err = shouldFail(IllegalStateException, '''
+            def r = switch (99) {
+                case 1 -> 1
+            }
+        ''')
+        assert err.message.contains('does not cover')
+    }
+
+    @Test
+    void groovyIsCaseMatching() {
+        assertScript '''
+            def r = switch ('abc') {
+                case String -> 'str'
+                case Integer -> 'int'
+                default -> 'other'
+            }
+            assert r == 'str'
+
+            r = switch (5) {
+                case 1..10 -> 'range'
+                default -> 'out'
+            }
+            assert r == 'range'
+
+            r = switch ('hello') {
+                case ~/h.*/ -> 're'
+                default -> 'no'
+            }
+            assert r == 're'
+
+            r = switch (4) {
+                case { it % 2 == 0 } -> 'even'
+                default -> 'odd'
+            }
+            assert r == 'even'
+        '''
+    }
+
+    @Test
+    void nestedSwitchExpressions() {
+        assertScript '''
+            def r = switch (1) {
+                case 1 -> switch (2) {
+                    case 2 -> 'inner'
+                    default -> 'x'
+                }
+                default -> 'outer'
+            }
+            assert r == 'inner'
+        '''
+    }
+
+    @Test
+    void usedAsStatement() {
+        assertScript '''
+            int n = 0
+            switch (1) {
+                case 1 -> n += 1
+                default -> n += 10
+            }
+            assert n == 1
+        '''
+    }
+
+    @Test
+    void assignToOuterLocal() {
+        assertScript '''
+            int acc = 0
+            def r = switch (1) {
+                case 1 -> {
+                    acc = 7
+                    yield acc
+                }
+                default -> 0
+            }
+            assert r == 7
+            assert acc == 7
+        '''
+    }
+
+    @Test
+    void compileStaticArrowAndYield() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def meth(int a) {
+                switch (a) {
+                    case 1 -> 'one'
+                    case 2 -> {
+                        yield 'two'
+                    }
+                    default -> 'many'
+                }
+            }
+            assert meth(1) == 'one'
+            assert meth(2) == 'two'
+            assert meth(9) == 'many'
+        '''
+    }
+
+    @Test
+    void compileStaticStringSwitch() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            String partner(String person) {
+                switch (person) {
+                    case 'Romeo' -> 'Juliet'
+                    case 'Adam' -> 'Eve'
+                    default -> 'Unknown'
+                }
+            }
+            assert partner('Romeo') == 'Juliet'
+            assert partner('Adam') == 'Eve'
+            assert partner('X') == 'Unknown'
+        '''
+    }
+
+    @Test
+    void compileStaticEnumSwitch() {
+        assertScript '''
+            import java.time.Month
+            import static java.time.Month.*
+
+            @groovy.transform.CompileStatic
+            String quarter(Month month) {
+                switch (month) {
+                    case JANUARY, FEBRUARY, MARCH -> 'Q1'
+                    case APRIL, MAY, JUNE -> 'Q2'
+                    case JULY, AUGUST, SEPTEMBER -> 'Q3'
+                    case OCTOBER, NOVEMBER, DECEMBER -> 'Q4'
+                }
+            }
+            assert quarter(JUNE) == 'Q2'
+            assert quarter(DECEMBER) == 'Q4'
+        '''
+    }
+
+    @Test
+    void yieldMethodNameOutsideSwitch() {
+        assertScript '''
+            def yield(String msg) { msg }
+            assert yield('ok') == 'ok'
+        '''
+    }
+
+    @Test
+    void primitiveResult() {
+        assertScript '''
+            int n = switch (2) {
+                case 1 -> 10
+                case 2 -> 20
+                default -> 0
+            }
+            assert n == 20
+        '''
+    }
+
+    @Test
+    void yieldInsideTryFinally() {
+        assertScript '''
+            def log = []
+            def r = switch (1) {
+                case 1 -> {
+                    try {
+                        yield 42
+                    } finally {
+                        log << 'fin'
+                    }
+                }
+                default -> 0
+            }
+            assert r == 42
+            assert log == ['fin']
+        '''
+    }
+
+    @Test
+    void nullSelectorUsesDefaultDynamically() {
+        assertScript '''
+            def r = switch (null) {
+                case 1 -> 'one'
+                default -> 'none'
+            }
+            assert r == 'none'
+        '''
+    }
+
+    @Test
+    void defaultOnly() {
+        assertScript '''
+            assert 7 == switch (99) {
+                default -> 7
+            }
+        '''
+    }
+
+    @Test
+    void tryFinallyAroundSwitchExpression() {
+        assertScript '''
+            def log = []
+            def r = null
+            try {
+                r = switch (1) {
+                    case 1 -> 42
+                    default -> 0
+                }
+            } finally {
+                log << 'outer'
+            }
+            assert r == 42
+            assert log == ['outer']
+        '''
+    }
+
+    @Test
+    void synchronizedAroundSwitchExpression() {
+        assertScript '''
+            def lock = new Object()
+            def r
+            synchronized (lock) {
+                r = switch (1) {
+                    case 1 -> 7
+                    default -> 0
+                }
+            }
+            assert r == 7
+        '''
+    }
+
+    @Test
+    void compileStaticDefiniteAssignmentAfterYield() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            int meth(int n) {
+                int x
+                int r = switch (n) {
+                    case 1 -> {
+                        x = 1
+                        yield 10
+                    }
+                    default -> {
+                        x = 2
+                        yield 20
+                    }
+                }
+                return r + x
+            }
+            assert meth(1) == 11
+            assert meth(0) == 22
+        '''
+    }
+
+    @Test
+    void nestedExpressionInsideSwitchStatementDifferentEnums() {
+        assertScript '''
+            enum Color { RED, BLUE }
+            enum Size { S, L }
+
+            @groovy.transform.CompileStatic
+            int meth(Color color, Size size) {
+                switch (color) {
+                    case RED:
+                        return switch (size) {
+                            case S -> 1
+                            case L -> 2
+                        }
+                    case BLUE:
+                        return switch (size) {
+                            case S -> 3
+                            case L -> 4
+                        }
+                }
+            }
+            assert meth(Color.RED, Size.S) == 1
+            assert meth(Color.BLUE, Size.L) == 4
+        '''
+    }
+
+    @Test
+    void returnInsideLoopInSwitchExpressionIsError() {
+        def err = shouldFail('''
+            def r = switch (1) {
+                case 1 -> {
+                    for (;;) {
+                        return 1
+                    }
+                }
+                default -> 0
+            }
+        ''')
+        assert err.message.contains('does not support `return`')
+    }
+
+    @Test
+    void switchExpressionInsideClosure() {
+        assertScript '''
+            def r = { int n ->
+                switch (n) {
+                    case 1 -> 'one'
+                    default -> 'other'
+                }
+            }(1)
+            assert r == 'one'
+        '''
+    }
+
+    @Test
+    void compileStaticNullStringSelectorUsesDefault() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            String m(String s) {
+                switch (s) {
+                    case 'Foo' -> 'a'
+                    case 'Bar' -> 'b'
+                    default    -> 'dflt'
+                }
+            }
+            assert m('Foo') == 'a'
+            assert m(null) == 'dflt'
+        '''
+    }
+
+    @Test
+    void compileStaticNullEnumSelectorUsesDefault() {
+        assertScript '''
+            import java.time.DayOfWeek
+
+            @groovy.transform.CompileStatic
+            String m(DayOfWeek d) {
+                switch (d) {
+                    case DayOfWeek.MONDAY -> 'mon'
+                    default -> 'dflt'
+                }
+            }
+            assert m(DayOfWeek.MONDAY) == 'mon'
+            assert m(null) == 'dflt'
+        '''
+    }
+
+    @Test
+    void compileStaticNullSelectorOnExhaustiveEnumThrows() {
+        def err = shouldFail(IllegalStateException, '''
+            enum Flag { ON, OFF }
+
+            @groovy.transform.CompileStatic
+            String m(Flag f) {
+                switch (f) {
+                    case Flag.ON  -> 'on'
+                    case Flag.OFF -> 'off'
+                }
+            }
+            m(null)
+        ''')
+        assert err.message.contains('does not cover')
+    }
+
+    @Test
+    void compileStaticNullIntegerSelectorUsesDefault() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            String m(Integer n) {
+                switch (n) {
+                    case 1 -> 'one'
+                    case 2 -> 'two'
+                    default -> 'dflt'
+                }
+            }
+            assert m(1) == 'one'
+            assert m(null) == 'dflt'
+        '''
+    }
+
+    @Test
+    void labeledBreakOutOfSwitchExpressionIsError() {
+        def err = shouldFail('''
+            outer:
+            while (true) {
+                def r = switch (1) {
+                    case 1 -> {
+                        for (;;) { break outer }
+                        yield -1
+                    }
+                    default -> 0
+                }
+            }
+        ''')
+        assert err.message.contains("cannot break to label 'outer'")
+    }
+
+    @Test
+    void labeledContinueOutOfSwitchExpressionIsError() {
+        def err = shouldFail('''
+            outer:
+            while (true) {
+                def r = switch (1) {
+                    case 1 -> {
+                        for (;;) { continue outer }
+                        yield -1
+                    }
+                    default -> 0
+                }
+            }
+        ''')
+        assert err.message.contains("cannot continue to label 'outer'")
+    }
+
+    @Test
+    void labeledBreakToArmLocalLoopIsAllowed() {
+        assertScript '''
+            def r = switch (1) {
+                case 1 -> {
+                    int n = 0
+                    inner:
+                    for (;;) {
+                        n += 1
+                        if (n > 2) break inner
+                    }
+                    yield n
+                }
+                default -> 0
+            }
+            assert r == 3
+        '''
+    }
+
+    @Test
+    void compileStaticEnumSwitchWithUnqualifiedConstantNames() {
+        assertScript '''
+            import java.time.Month
+
+            @groovy.transform.CompileStatic
+            String m(Month month) {
+                switch (month) {
+                    case JANUARY -> 'jan'
+                    case JUNE -> 'jun'
+                    default -> 'other'
+                }
+            }
+            assert m(Month.JANUARY) == 'jan'
+            assert m(Month.JUNE) == 'jun'
+            assert m(Month.MARCH) == 'other'
+        '''
+    }
+
+    @Test
+    void compileStaticEnumSwitchLocalVariableShadowingConstantName() {
+        assertScript '''
+            import java.time.Month
+
+            @groovy.transform.CompileStatic
+            String m(Month month, Month JANUARY) {
+                switch (month) {
+                    case JANUARY -> 'matched local'
+                    default -> 'other'
+                }
+            }
+            assert m(Month.JUNE, Month.JUNE) == 'matched local'
+            assert m(Month.JANUARY, Month.JUNE) == 'other'
+        '''
+    }
+
+    @Test
+    void compileStaticStringSwitchWithHashCollision() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            String m(String s) {
+                switch (s) {
+                    case 'Aa' -> 'first'   // 'Aa' and 'BB' share a hashCode,
+                    case 'BB' -> 'second'  // exercising the equals chain
+                    default   -> 'none'
+                }
+            }
+            assert m('Aa') == 'first'
+            assert m('BB') == 'second'
+            assert m('Cc') == 'none'
+        '''
+    }
+
+    @Test
+    void compileStaticSparseIntKeysStillDispatch() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            int m(int n) {
+                switch (n) {
+                    case 1       -> 10
+                    case 100     -> 20
+                    case 1000000 -> 30
+                    default      -> 0
+                }
+            }
+            assert m(1) == 10
+            assert m(100) == 20
+            assert m(1000000) == 30
+            assert m(7) == 0
+        '''
+    }
+
+    @Test
+    void compileStaticNonConstantLabelFallsBackToIsCase() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            String m(int n) {
+                switch (n) {
+                    case 1        -> 'one'
+                    case 300..400 -> 'range'
+                    default       -> 'other'
+                }
+            }
+            assert m(1) == 'one'
+            assert m(350) == 'range'
+            assert m(7) == 'other'
+        '''
+    }
+
+    @Test
+    void switchExpressionNodeApi() {
+        def se = GeneralUtils.switchX(GeneralUtils.constX(1),
+                [new CaseStatement(GeneralUtils.constX(1), 
GeneralUtils.stmt(GeneralUtils.constX('a')))],
+                GeneralUtils.stmt(GeneralUtils.constX('z')))
+        se.addCase(new CaseStatement(GeneralUtils.constX(2), 
GeneralUtils.yieldS(GeneralUtils.constX('b'))))
+        assert se.text.startsWith('switch (')
+        assert se.toString().contains('cases')
+        se.expression = GeneralUtils.constX(3)
+        assert se.expression.text == '3'
+        se.defaultStatement = GeneralUtils.yieldS(GeneralUtils.constX('y'))
+        assert se.defaultStatement instanceof YieldStatement
+        assert se.defaultStatement.text == "yield y"
+        def copy = se.transformExpression(new ExpressionTransformer() {
+            @Override
+            Expression transform(Expression expression) { expression }
+        })
+        assert copy instanceof SwitchExpression
+        assert copy.caseStatements.size() == 2
+        assert copy.caseStatements[1].arrow == se.caseStatements[1].arrow
+    }
+
+    @Test
+    void yieldThroughNestedClosureIsError() {
+        def err = shouldFail('''
+            def r = switch (1) {
+                case 1 -> {
+                    def c = { yield 1 }
+                    yield c()
+                }
+                default -> 0
+            }
+        ''')
+        assert err.message.contains('yield cannot jump through a closure or 
lambda')

Review Comment:
   Added:
   
   1. `asyncYieldReturnInsideNestedClosureIsAllowed` — `yield return` inside 
`async { }` nested in a switch arm (generator yield, not switch `yield`).
   2. `switchStatementInsideSwitchExpressionCanYield` — a colon `switch` 
*statement* whose `yield` completes the enclosing switch *expression*.
   





> 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
>            Priority: Major
>              Labels: breaking
>
> 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)

Reply via email to