[
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)