[
https://issues.apache.org/jira/browse/GROOVY-12255?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18105357#comment-18105357
]
ASF GitHub Bot commented on GROOVY-12255:
-----------------------------------------
blackdrag commented on code in PR #2784:
URL: https://github.com/apache/groovy/pull/2784#discussion_r3797730528
##########
src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy:
##########
@@ -0,0 +1,1073 @@
+/*
+ * 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.ClassCodeExpressionTransformer
+import org.codehaus.groovy.ast.expr.ConstantExpression
+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.AssertStatement
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.codehaus.groovy.control.SourceUnit
+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 compileStaticForLoopInArmWithImplicitThis() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ class C {
+ int n
+ int run() {
+ switch (1) {
+ case 1 -> {
+ for (int i = 0; i < 3; i++) {
+ bump()
+ }
+ yield n
+ }
+ default -> 0
+ }
+ }
+ void bump() { n += 1 }
+ }
+ assert new C().run() == 3
+ '''
+ }
+
+ @Test
+ void labeledBreakSkippingYieldInLastArmIsError() {
+ def err = shouldFail('''
+ def cond = true
+ def r = switch (1) {
+ case 1 -> {
+ label:
+ if (cond) break label
+ yield 1
+ }
+ default -> 0
+ }
+ ''')
+ assert err.message.contains('does not support `break`') ||
err.message.contains('yield')
+ }
+
+ @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
+ assert copy.caseStatements[0].code.is(se.caseStatements[0].code)
+ assert copy.defaultStatement.is(se.defaultStatement)
+ }
+
+ @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')
+ }
+
+ @Test
+ void asyncYieldReturnInsideNestedClosureIsAllowed() {
+ assertScript '''
+ def r = switch (1) {
+ case 1 -> {
+ def items = async {
+ yield return 7
+ }
+ yield items.collect().first()
+ }
+ default -> 0
+ }
+ assert r == 7
+ '''
+ }
+
+ @Test
+ void switchStatementInsideSwitchExpressionCanYield() {
+ assertScript '''
+ def r = switch (1) {
+ case 1 -> {
+ switch (2) {
+ case 2:
+ yield 42
+ default:
+ yield 0
+ }
+ }
+ default -> -1
+ }
+ assert r == 42
+ '''
+ }
+
+ @Test
+ void colonArmIfFallsThroughToCompletingDefault() {
+ assertScript '''
+ def cond = false
+ def r = switch ('a') {
+ case 'a':
+ if (cond) yield 1
+ default:
+ yield 0
+ }
+ assert r == 0
+ '''
+ }
+
+ @Test
+ void lastColonArmIfWithoutElseIsError() {
+ def err = shouldFail('''
+ def r = switch (1) {
+ case 1:
+ if (true) yield 1
+ }
+ ''')
+ assert err.message.contains('yield') || err.message.contains('throw')
+ }
+
+ @Test
+ void compileStaticYieldInsideTryFinallyIntSwitch() {
Review Comment:
why is this specific to static compilation?
##########
src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy:
##########
@@ -0,0 +1,1073 @@
+/*
+ * 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.ClassCodeExpressionTransformer
+import org.codehaus.groovy.ast.expr.ConstantExpression
+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.AssertStatement
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.codehaus.groovy.control.SourceUnit
+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'")
Review Comment:
This is a compilation error, right? You should assert the exception type as
well.
##########
src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy:
##########
@@ -0,0 +1,1073 @@
+/*
+ * 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.ClassCodeExpressionTransformer
+import org.codehaus.groovy.ast.expr.ConstantExpression
+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.AssertStatement
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.codehaus.groovy.control.SourceUnit
+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'")
Review Comment:
same case as for continue
##########
src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesSwitchExpressionWriter.java:
##########
@@ -0,0 +1,439 @@
+/*
+ * 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.asm.sc;
+
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
+import org.codehaus.groovy.ast.stmt.CaseStatement;
+import org.codehaus.groovy.classgen.AsmClassGenerator;
+import org.codehaus.groovy.classgen.asm.CompileStack;
+import org.codehaus.groovy.classgen.asm.OperandStack;
+import org.codehaus.groovy.classgen.asm.SwitchExpressionWriter;
+import org.codehaus.groovy.classgen.asm.VariableSlotLoader;
+import org.codehaus.groovy.syntax.SyntaxException;
+import org.codehaus.groovy.transform.stc.StaticTypesMarker;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Function;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod;
+import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsByNameAndArguments;
+import static org.objectweb.asm.Opcodes.GOTO;
+import static org.objectweb.asm.Opcodes.IFEQ;
+import static org.objectweb.asm.Opcodes.IFNULL;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+
+/**
+ * Static-compilation writer for {@link SwitchExpression}. Emits
+ * {@code tableswitch} / {@code lookupswitch} when the selector and labels are
+ * constants of a type {@code javac} would switch on, and otherwise a resolved
+ * {@code isCase} call rather than a forced dynamic adapter invocation.
+ *
+ * @since 6.0.0
+ */
+public class StaticTypesSwitchExpressionWriter extends SwitchExpressionWriter {
+
+ /**
+ * Creates a switch-expression writer for statically compiled methods.
+ *
+ * @param controller the static types writer controller
+ */
+ public StaticTypesSwitchExpressionWriter(final StaticTypesWriterController
controller) {
+ super(controller);
+ }
+
+ /**
+ * Keeps the selector as visited — boxing is deferred until a path actually
+ * needs a reference (isCase or a null check on a wrapper).
+ */
+ @Override
+ protected ClassNode prepareSelectorType(final OperandStack operandStack) {
+ return operandStack.getTopOperand();
+ }
+
+ @Override
+ protected boolean writeOptimizedSwitch(final SwitchExpression expression,
+ final int selectorIndex, final ClassNode selectorType) {
+ return writeIntSwitch(expression, selectorIndex, selectorType)
+ || writeStringSwitch(expression, selectorIndex, selectorType)
+ || writeEnumSwitch(expression, selectorIndex, selectorType);
+ }
+
+ /**
+ * Prefers a statically resolved {@code isCase} (DGM overload or instance
+ * method) so Class / Collection / Pattern / Closure labels stay correct
+ * without going through {@code ScriptBytecodeAdapter}.
+ */
+ @Override
+ protected void writeIsCaseComparison(final Expression caseValue,
+ final int selectorIndex, final ClassNode selectorType) {
+ MethodNode target = resolveIsCaseTarget(caseValue, selectorType);
+ if (target == null) {
+ super.writeIsCaseComparison(caseValue, selectorIndex,
selectorType);
+ return;
+ }
+ OperandStack operandStack = controller.getOperandStack();
+ VariableSlotLoader selector = new VariableSlotLoader(selectorType,
selectorIndex, operandStack);
+ MethodCallExpression call = callX(caseValue, "isCase", args(selector));
+ call.setImplicitThis(false);
+ call.setMethodTarget(target);
+ call.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET,
target);
+ call.putNodeMetaData(StaticTypesMarker.INFERRED_TYPE,
ClassHelper.boolean_TYPE);
+ call.setSourcePosition(caseValue);
+ call.visit(controller.getAcg());
+ operandStack.doGroovyCast(ClassHelper.boolean_TYPE);
+ }
+
+ private MethodNode resolveIsCaseTarget(final Expression caseValue, final
ClassNode selectorType) {
+ ClassNode caseType =
controller.getTypeChooser().resolveType(caseValue, controller.getClassNode());
+ ClassNode switchArg = ClassHelper.isPrimitiveType(selectorType)
+ ? ClassHelper.getWrapper(selectorType) : selectorType;
Review Comment:
minor: getWrapper is already doing this check for you.
##########
src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java:
##########
@@ -4878,6 +4880,125 @@ public void visitSwitch(final SwitchStatement
statement) {
}
}
+ /**
+ * Type-checks a switch expression: visits the selector and each arm,
unifies
+ * the yielded types (JEP 361 poly expression), and reports a
non-exhaustive
+ * switch when the selector type is statically known (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ @Override
+ public void visitSwitchExpression(final SwitchExpression expression) {
Review Comment:
I already mentioned it before but I think I was way to unspecific and then
you misunderstood me. There are basically 2 cases for the switch expression:
the generic isCase variant and the intrinsic variant. I think you need to check
isCase here and if it is the isCase variant, you should actually go through
method selection here to have a direct method call target be chosen for isCase.
If there is no target this is a compilation error. This should then handle
instance and DGM, other extensions, as well as making the typechecking
extensions work for the isCase call. The isCase write is then actually a
direct method call write only, which is handled by writeDirectMethodCall in
StaticInvocationWriter. The intrinsic cases are to be handled by
StaticTypesSwitchExpressionWriter directly, while for isCase should then go
through the invocation writer mechanism.
##########
src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java:
##########
@@ -0,0 +1,193 @@
+/*
+ * 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.ast.expr;
+
+import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
+import org.codehaus.groovy.ast.GroovyCodeVisitor;
+import org.codehaus.groovy.ast.stmt.CaseStatement;
+import org.codehaus.groovy.ast.stmt.EmptyStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.SwitchStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a {@code switch} used as an expression, as specified by
+ * JEP 361 (Switch Expressions). The selector is evaluated once and matched
+ * against the {@link CaseStatement} list using Groovy's {@code isCase}
+ * semantics (or a tableswitch / lookupswitch when the compiler can prove
+ * that is equivalent). Each completing arm yields a value via
+ * {@link YieldStatement}; the expression's result is that value.
+ * <p>
+ * Arms stay as {@link CaseStatement}s, the same way a
+ * {@link ClosureExpression} holds a statement body: the case label is an
+ * expression, the arm is a statement. A plain {@link ExpressionTransformer}
+ * rewrites only the selector and case labels. A
+ * {@link ClassCodeExpressionTransformer} walks the tree in place via
+ * {@link ClassCodeExpressionTransformer#visitSwitchExpression}.
+ *
+ * @see SwitchStatement
+ * @see YieldStatement
+ * @see CaseStatement
+ * @since 6.0.0
+ */
+public class SwitchExpression extends Expression {
+
+ private Expression expression;
+ private List<CaseStatement> caseStatements;
+ private Statement defaultStatement;
+
+ /**
+ * Constructs a switch expression with the given selector.
+ * The default statement is initialized to {@link EmptyStatement#INSTANCE}.
+ *
+ * @param expression the selector expression
+ */
+ public SwitchExpression(final Expression expression) {
+ this(expression, EmptyStatement.INSTANCE);
+ }
+
+ /**
+ * Constructs a switch expression with the given selector and default arm.
+ *
+ * @param expression the selector expression
+ * @param defaultStatement the arm executed when no case matches; may be
{@link EmptyStatement#INSTANCE}
+ */
+ public SwitchExpression(final Expression expression, final Statement
defaultStatement) {
+ this(expression, new ArrayList<>(), defaultStatement);
+ }
+
+ /**
+ * Constructs a switch expression with the given selector, case arms, and
default arm.
+ *
+ * @param expression the selector expression
+ * @param caseStatements the case arms
+ * @param defaultStatement the arm executed when no case matches
+ */
+ public SwitchExpression(final Expression expression, final
List<CaseStatement> caseStatements, final Statement defaultStatement) {
+ this.expression = expression;
+ this.caseStatements = caseStatements;
+ this.defaultStatement = defaultStatement;
+ }
+
+ /**
+ * Returns the selector expression matched against case values.
+ *
+ * @return the selector {@link Expression}
+ */
+ public Expression getExpression() {
+ return expression;
+ }
+
+ /**
+ * Sets the selector expression matched against case values.
+ *
+ * @param expression the selector {@link Expression}
+ */
+ public void setExpression(final Expression expression) {
+ this.expression = expression;
+ }
+
+ /**
+ * Returns the case arms of this switch expression.
+ *
+ * @return a list of {@link CaseStatement} objects; never null
+ */
+ public List<CaseStatement> getCaseStatements() {
+ return caseStatements;
+ }
+
+ /**
+ * Returns the arm executed when no case matches.
+ *
+ * @return the default {@link Statement}, or {@link
EmptyStatement#INSTANCE} if not set
+ */
+ public Statement getDefaultStatement() {
+ return defaultStatement;
+ }
+
+ /**
+ * Sets the arm executed when no case matches.
+ *
+ * @param defaultStatement the default {@link Statement}
+ */
+ public void setDefaultStatement(final Statement defaultStatement) {
+ this.defaultStatement = defaultStatement;
+ }
+
+ /**
+ * Adds a case arm to this switch expression.
+ *
+ * @param caseStatement the {@link CaseStatement} to add
+ */
+ public void addCase(final CaseStatement caseStatement) {
+ caseStatements.add(caseStatement);
+ }
+
+ @Override
+ public String getText() {
+ return "switch (" + expression.getText() + ") { ... }";
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "[expression: " + expression + "; cases: " +
caseStatements + "; default: " + defaultStatement + "]";
+ }
+
+ /**
+ * A {@link ClassCodeExpressionTransformer} walks this node in place
through
+ * {@link ClassCodeExpressionTransformer#visitSwitchExpression}, the same
+ * pattern {@link ClosureExpression} uses so resolve, static-import and
+ * similar rewrites still see nested arm expressions. Any other transformer
+ * gets a copy of the selector and case labels; arm statements are shared.
+ */
+ @Override
+ public Expression transformExpression(final ExpressionTransformer
transformer) {
+ if (transformer instanceof ClassCodeExpressionTransformer visitor) {
+ visitor.visitSwitchExpression(this);
+ return this;
+ }
Review Comment:
I think this code should be in ClassCodeExpressionTransformer. The other
part of the method looks good to me now
##########
src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy:
##########
@@ -0,0 +1,1073 @@
+/*
+ * 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.ClassCodeExpressionTransformer
+import org.codehaus.groovy.ast.expr.ConstantExpression
+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.AssertStatement
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.codehaus.groovy.control.SourceUnit
+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 compileStaticForLoopInArmWithImplicitThis() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ class C {
+ int n
+ int run() {
+ switch (1) {
+ case 1 -> {
+ for (int i = 0; i < 3; i++) {
+ bump()
+ }
+ yield n
+ }
+ default -> 0
+ }
+ }
+ void bump() { n += 1 }
+ }
+ assert new C().run() == 3
+ '''
+ }
+
+ @Test
+ void labeledBreakSkippingYieldInLastArmIsError() {
+ def err = shouldFail('''
+ def cond = true
+ def r = switch (1) {
+ case 1 -> {
+ label:
+ if (cond) break label
+ yield 1
+ }
+ default -> 0
+ }
+ ''')
+ assert err.message.contains('does not support `break`') ||
err.message.contains('yield')
+ }
+
+ @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
Review Comment:
I am randomly using this line to anchor the comment, but it actually is
about several places with CompileStatic. You already have tests that ensure
the bytecode contains structures you expect from static compilation. Which
means I assume the test here is to finalize that the behavior is consistent.
But for this you need a base to compare to, which should be the same test
without static compilation. So I suggest you do something like
```
def script = """..."""
assertScript script
assertScript "@groovy.transform.CompileStatic\n" + script
```
And that way you ensure baseline (dynamic Groovy) and static compiler align
in behavior. You should look at each @CompileStatic using test in this class
and ask yourself if it is really specific to the static compiler. If not you
should change it like suggested, if it is special, then is should go into the
static compilation test suite instead - or at least the test should have
something explaining why this is only with static compilation.
##########
src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy:
##########
@@ -0,0 +1,1073 @@
+/*
+ * 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.ClassCodeExpressionTransformer
+import org.codehaus.groovy.ast.expr.ConstantExpression
+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.AssertStatement
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.codehaus.groovy.control.SourceUnit
+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 compileStaticForLoopInArmWithImplicitThis() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ class C {
+ int n
+ int run() {
+ switch (1) {
+ case 1 -> {
+ for (int i = 0; i < 3; i++) {
+ bump()
+ }
+ yield n
+ }
+ default -> 0
+ }
+ }
+ void bump() { n += 1 }
+ }
+ assert new C().run() == 3
+ '''
+ }
+
+ @Test
+ void labeledBreakSkippingYieldInLastArmIsError() {
+ def err = shouldFail('''
+ def cond = true
+ def r = switch (1) {
+ case 1 -> {
+ label:
+ if (cond) break label
+ yield 1
+ }
+ default -> 0
+ }
+ ''')
+ assert err.message.contains('does not support `break`') ||
err.message.contains('yield')
+ }
+
+ @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
+ assert copy.caseStatements[0].code.is(se.caseStatements[0].code)
+ assert copy.defaultStatement.is(se.defaultStatement)
+ }
+
+ @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')
+ }
+
+ @Test
+ void asyncYieldReturnInsideNestedClosureIsAllowed() {
+ assertScript '''
+ def r = switch (1) {
+ case 1 -> {
+ def items = async {
+ yield return 7
+ }
+ yield items.collect().first()
+ }
+ default -> 0
+ }
+ assert r == 7
+ '''
+ }
+
+ @Test
+ void switchStatementInsideSwitchExpressionCanYield() {
+ assertScript '''
+ def r = switch (1) {
+ case 1 -> {
+ switch (2) {
+ case 2:
+ yield 42
+ default:
+ yield 0
+ }
+ }
+ default -> -1
+ }
+ assert r == 42
+ '''
+ }
+
+ @Test
+ void colonArmIfFallsThroughToCompletingDefault() {
+ assertScript '''
+ def cond = false
+ def r = switch ('a') {
+ case 'a':
+ if (cond) yield 1
+ default:
+ yield 0
+ }
+ assert r == 0
+ '''
+ }
+
+ @Test
+ void lastColonArmIfWithoutElseIsError() {
+ def err = shouldFail('''
+ def r = switch (1) {
+ case 1:
+ if (true) yield 1
+ }
+ ''')
+ assert err.message.contains('yield') || err.message.contains('throw')
+ }
+
+ @Test
+ void compileStaticYieldInsideTryFinallyIntSwitch() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ int m(int n) {
+ def log = []
+ int r = switch (n) {
+ case 1 -> {
+ try {
+ yield 10
+ } finally {
+ log << 'fin'
+ }
+ }
+ default -> 0
+ }
+ assert log == ['fin']
+ return r
+ }
+ assert m(1) == 10
+ '''
+ }
+
+ @Test
+ void compileStaticYieldInsideTryFinallyStringSwitch() {
Review Comment:
why is this specific to static compilation? Also why does it matter if r is
int or String?
##########
src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy:
##########
@@ -0,0 +1,1073 @@
+/*
+ * 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.ClassCodeExpressionTransformer
+import org.codehaus.groovy.ast.expr.ConstantExpression
+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.AssertStatement
+import org.codehaus.groovy.ast.stmt.CaseStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.codehaus.groovy.control.SourceUnit
+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 compileStaticForLoopInArmWithImplicitThis() {
+ assertScript '''
+ @groovy.transform.CompileStatic
+ class C {
+ int n
+ int run() {
+ switch (1) {
+ case 1 -> {
+ for (int i = 0; i < 3; i++) {
+ bump()
+ }
+ yield n
+ }
+ default -> 0
+ }
+ }
+ void bump() { n += 1 }
+ }
+ assert new C().run() == 3
+ '''
+ }
+
+ @Test
+ void labeledBreakSkippingYieldInLastArmIsError() {
+ def err = shouldFail('''
+ def cond = true
+ def r = switch (1) {
+ case 1 -> {
+ label:
+ if (cond) break label
+ yield 1
+ }
+ default -> 0
+ }
+ ''')
+ assert err.message.contains('does not support `break`') ||
err.message.contains('yield')
+ }
+
+ @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
+ assert copy.caseStatements[0].code.is(se.caseStatements[0].code)
+ assert copy.defaultStatement.is(se.defaultStatement)
+ }
+
+ @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')
+ }
+
+ @Test
+ void asyncYieldReturnInsideNestedClosureIsAllowed() {
+ assertScript '''
+ def r = switch (1) {
+ case 1 -> {
+ def items = async {
+ yield return 7
+ }
+ yield items.collect().first()
+ }
+ default -> 0
+ }
+ assert r == 7
+ '''
+ }
+
+ @Test
+ void switchStatementInsideSwitchExpressionCanYield() {
+ assertScript '''
+ def r = switch (1) {
+ case 1 -> {
+ switch (2) {
+ case 2:
+ yield 42
+ default:
+ yield 0
+ }
+ }
+ default -> -1
+ }
+ assert r == 42
+ '''
+ }
+
+ @Test
+ void colonArmIfFallsThroughToCompletingDefault() {
+ assertScript '''
+ def cond = false
+ def r = switch ('a') {
+ case 'a':
+ if (cond) yield 1
+ default:
+ yield 0
+ }
+ assert r == 0
+ '''
+ }
+
+ @Test
+ void lastColonArmIfWithoutElseIsError() {
+ def err = shouldFail('''
+ def r = switch (1) {
+ case 1:
+ if (true) yield 1
+ }
+ ''')
+ assert err.message.contains('yield') || err.message.contains('throw')
Review Comment:
again the question as of why yield *or* throw?
##########
src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticTypesSwitchExpressionWriter.java:
##########
@@ -0,0 +1,439 @@
+/*
+ * 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.asm.sc;
+
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
+import org.codehaus.groovy.ast.stmt.CaseStatement;
+import org.codehaus.groovy.classgen.AsmClassGenerator;
+import org.codehaus.groovy.classgen.asm.CompileStack;
+import org.codehaus.groovy.classgen.asm.OperandStack;
+import org.codehaus.groovy.classgen.asm.SwitchExpressionWriter;
+import org.codehaus.groovy.classgen.asm.VariableSlotLoader;
+import org.codehaus.groovy.syntax.SyntaxException;
+import org.codehaus.groovy.transform.stc.StaticTypesMarker;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Function;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod;
+import static
org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsByNameAndArguments;
+import static org.objectweb.asm.Opcodes.GOTO;
+import static org.objectweb.asm.Opcodes.IFEQ;
+import static org.objectweb.asm.Opcodes.IFNULL;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+
+/**
+ * Static-compilation writer for {@link SwitchExpression}. Emits
+ * {@code tableswitch} / {@code lookupswitch} when the selector and labels are
+ * constants of a type {@code javac} would switch on, and otherwise a resolved
+ * {@code isCase} call rather than a forced dynamic adapter invocation.
+ *
+ * @since 6.0.0
+ */
+public class StaticTypesSwitchExpressionWriter extends SwitchExpressionWriter {
+
+ /**
+ * Creates a switch-expression writer for statically compiled methods.
+ *
+ * @param controller the static types writer controller
+ */
+ public StaticTypesSwitchExpressionWriter(final StaticTypesWriterController
controller) {
+ super(controller);
+ }
+
+ /**
+ * Keeps the selector as visited — boxing is deferred until a path actually
+ * needs a reference (isCase or a null check on a wrapper).
+ */
+ @Override
+ protected ClassNode prepareSelectorType(final OperandStack operandStack) {
+ return operandStack.getTopOperand();
+ }
+
+ @Override
+ protected boolean writeOptimizedSwitch(final SwitchExpression expression,
+ final int selectorIndex, final ClassNode selectorType) {
+ return writeIntSwitch(expression, selectorIndex, selectorType)
+ || writeStringSwitch(expression, selectorIndex, selectorType)
+ || writeEnumSwitch(expression, selectorIndex, selectorType);
+ }
+
+ /**
+ * Prefers a statically resolved {@code isCase} (DGM overload or instance
+ * method) so Class / Collection / Pattern / Closure labels stay correct
+ * without going through {@code ScriptBytecodeAdapter}.
+ */
+ @Override
+ protected void writeIsCaseComparison(final Expression caseValue,
+ final int selectorIndex, final ClassNode selectorType) {
+ MethodNode target = resolveIsCaseTarget(caseValue, selectorType);
+ if (target == null) {
+ super.writeIsCaseComparison(caseValue, selectorIndex,
selectorType);
+ return;
+ }
+ OperandStack operandStack = controller.getOperandStack();
+ VariableSlotLoader selector = new VariableSlotLoader(selectorType,
selectorIndex, operandStack);
+ MethodCallExpression call = callX(caseValue, "isCase", args(selector));
+ call.setImplicitThis(false);
+ call.setMethodTarget(target);
+ call.putNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET,
target);
+ call.putNodeMetaData(StaticTypesMarker.INFERRED_TYPE,
ClassHelper.boolean_TYPE);
+ call.setSourcePosition(caseValue);
+ call.visit(controller.getAcg());
+ operandStack.doGroovyCast(ClassHelper.boolean_TYPE);
+ }
+
+ private MethodNode resolveIsCaseTarget(final Expression caseValue, final
ClassNode selectorType) {
+ ClassNode caseType =
controller.getTypeChooser().resolveType(caseValue, controller.getClassNode());
+ ClassNode switchArg = ClassHelper.isPrimitiveType(selectorType)
+ ? ClassHelper.getWrapper(selectorType) : selectorType;
+ MethodNode instance = chooseInstanceIsCase(caseType, switchArg);
+ if (instance != null) {
+ return instance;
+ }
+ ClassLoader loader = controller.getSourceUnit().getClassLoader();
+ List<MethodNode> methods = findDGMMethodsByNameAndArguments(
+ loader, caseType, "isCase", new ClassNode[]{switchArg});
+ if (methods.size() != 1) {
+ return null;
+ }
+ MethodNode dgm = methods.get(0);
+ // Object.equals-style isCase is not a substitute for runtime dispatch
+ if (isGenericObjectIsCase(dgm)) {
+ return null;
+ }
+ return dgm;
+ }
+
+ private static MethodNode chooseInstanceIsCase(final ClassNode caseType,
final ClassNode switchArg) {
+ if (caseType == null) return null;
Review Comment:
in combination with my comment on StaticTypeCheckingVisitor you would
actually only have to check for the direct method call target here and then
write it, if it exists.
> 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)