You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/08/20 04:43:37 UTC
(groovy) 01/11: GROOVY-12255: Compile switch expressions as first-class AST (no closure desugar)
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit f763b8a11f17efc33e17221f4516109bd3865cb0
Author: Daniel Sun <su...@apache.org>
AuthorDate: Fri Aug 14 02:05:47 2026 +0900
GROOVY-12255: Compile switch expressions as first-class AST (no closure desugar)
---
COMPATIBILITY.md | 23 +
.../apache/groovy/parser/antlr4/AstBuilder.java | 160 +++---
.../groovy/ast/ClassCodeExpressionTransformer.java | 16 +
.../groovy/ast/ClassCodeVisitorSupport.java | 14 +
.../codehaus/groovy/ast/CodeVisitorSupport.java | 52 ++
.../org/codehaus/groovy/ast/GroovyCodeVisitor.java | 31 ++
.../groovy/ast/GroovyCodeVisitorAdapter.java | 24 +
.../codehaus/groovy/ast/expr/SwitchExpression.java | 201 +++++++
.../org/codehaus/groovy/ast/query/AstQuery.java | 2 +
.../codehaus/groovy/ast/stmt/CaseStatement.java | 26 +-
.../codehaus/groovy/ast/stmt/YieldStatement.java | 81 +++
.../codehaus/groovy/ast/tools/GeneralUtils.java | 29 +-
.../groovy/classgen/AsmClassGenerator.java | 19 +
.../groovy/classgen/FinalVariableAnalyzer.java | 85 ++-
.../groovy/classgen/VariableScopeVisitor.java | 10 +
.../codehaus/groovy/classgen/asm/CompileStack.java | 73 +++
.../groovy/classgen/asm/DelegatingController.java | 8 +
.../classgen/asm/SwitchExpressionWriter.java | 578 +++++++++++++++++++++
.../groovy/classgen/asm/WriterController.java | 12 +
.../org/codehaus/groovy/control/LabelVerifier.java | 59 +++
.../control/customizers/SecureASTCustomizer.java | 31 ++
.../transform/StaticTypesTransformation.java | 5 +-
.../transform/stc/EnumTypeCheckingExtension.java | 14 +-
.../transform/stc/StaticTypeCheckingVisitor.java | 127 ++++-
.../groovy/transform/stc/TypeCheckingContext.java | 70 +++
src/spec/doc/core-semantics.adoc | 28 +-
src/spec/test/SemanticsTest.groovy | 17 +
.../core/SwitchExpression_02x.groovy | 10 +-
.../core/SwitchExpression_08x.groovy | 10 +-
.../core/SwitchExpression_20x.groovy | 5 +-
.../core/SwitchExpression_21x.groovy | 1 +
.../SwitchExpression_11x.groovy} | 18 +-
.../SwitchExpression_12x.groovy} | 18 +-
.../SwitchExpression_13x.groovy} | 18 +-
.../transform/stc/SwitchExpressionSTCTest.groovy | 129 +++++
.../groovy/parser/antlr4/SyntaxErrorTest.groovy | 3 +
.../groovy/parser/antlr4/util/AstDumper.groovy | 28 +
.../codehaus/groovy/classgen/Groovy12255.groovy | 434 ++++++++++++++++
.../asm/SwitchExpressionBytecodeTest.groovy | 87 ++++
.../console/ui/AstNodeToScriptAdapter.groovy | 30 ++
.../console/ui/ScriptToTreeNodeAdapter.groovy | 14 +
.../codehaus/groovy/macro/matcher/ASTFinder.java | 14 +
.../macro/matcher/ContextualClassCodeVisitor.java | 16 +
43 files changed, 2510 insertions(+), 120 deletions(-)
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 062e9d09ec..1676a75ada 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -260,6 +260,29 @@ back-port silently.
Release notes for the 6.0 beta line should call this out (JIRA
`breaking` label on GROOVY-12242).
+### Groovy 6 — first-class switch expressions (GROOVY-12255)
+
+Groovy 6 compiles switch expressions (JEP 361) as first-class AST
+(`SwitchExpression` / `YieldStatement`) instead of desugaring them to an
+immediately-called closure around a switch statement (the GROOVY-9272
+implementation shipped in 4.x / 5.x).
+
+**Who is affected (runtime behaviour).** A dynamic switch expression whose
+selector matches no arm now throws `IllegalStateException`. Previously the
+desugared closure completed without a `return` and the expression evaluated
+to `null`. Under `@TypeChecked` / `@CompileStatic`, a non-exhaustive switch
+expression (no `default`, and not a complete enum) is a compile-time error.
+
+**Who is affected (AST tools).** Visitors, macros, and AST transforms that
+assumed a switch expression was a `MethodCallExpression` wrapping a
+`SwitchStatement` need to handle `SwitchExpression` and `YieldStatement`.
+`GroovyCodeVisitor` supplies default methods so existing visitors keep
+compiling.
+
+**What is *not* claimed.** Matching still uses Groovy `isCase` (Class, regex,
+Collection, Closure). `tableswitch` / `lookupswitch` are emitted only when
+the selector and labels are constants of a type `javac` would switch on.
+
### Groovy 6 — `CompilerConfiguration` copy constructor copies customizers (GROOVY-9585)
`CompilerConfiguration(CompilerConfiguration)` now copies the source
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 78eabfdcf9..6c1420bb06 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -99,6 +99,7 @@ import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -122,6 +123,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.ast.tools.ClosureUtils;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.control.CompilationFailedException;
@@ -171,6 +173,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.declX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.yieldS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
import static org.codehaus.groovy.classgen.asm.util.TypeUtil.isPrimitiveType;
import static org.codehaus.groovy.runtime.DefaultGroovyMethods.asBoolean;
@@ -922,7 +925,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
@Override
public ReturnStatement visitReturnStmtAlt(final ReturnStmtAltContext ctx) {
- if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
+ if (isInsideSwitchExpression()) {
throw createParsingFailedException("switch expression does not support `return`", ctx);
}
@@ -962,10 +965,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
@Override
- public ReturnStatement visitYieldStatement(final YieldStatementContext ctx) {
- ReturnStatement returnStatement = (ReturnStatement) returnS((Expression) this.visit(ctx.expression()));
- returnStatement.putNodeMetaData(IS_YIELD_STATEMENT, Boolean.TRUE);
- return configureAST(returnStatement, ctx);
+ public YieldStatement visitYieldStatement(final YieldStatementContext ctx) {
+ return configureAST(yieldS((Expression) this.visit(ctx.expression())), ctx);
}
@Override
@@ -975,7 +976,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
@Override
- public ReturnStatement visitYieldStmtAlt(final YieldStmtAltContext ctx) {
+ public YieldStatement visitYieldStmtAlt(final YieldStmtAltContext ctx) {
return configureAST(this.visitYieldStatement(ctx.yieldStatement()), ctx);
}
@@ -1020,27 +1021,13 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
/**
- * <pre>
- * switch(x) {
- * case 0, 1 -> 'a'
- * case 2 -> 'b'
- * default -> 'z'
- * }
- * </pre>
- * will be transformed to:
- * <pre>
- * { ->
- * switch(x) {
- * case 0:
- * case 1: return 'a'
- * case 2: return 'b'
- * default: return 'z'
- * }
- * }.call()
- * </pre>
+ * Builds a first-class {@link SwitchExpression} (GROOVY-12255 / JEP 361).
+ * Arrow arms that are a single expression become {@link YieldStatement}s;
+ * colon arms use explicit {@code yield}. The expression is compiled inline
+ * — it is not rewritten to a closure wrapping a {@link SwitchStatement}.
*/
@Override
- public MethodCallExpression visitSwitchExpression(final SwitchExpressionContext ctx) {
+ public SwitchExpression visitSwitchExpression(final SwitchExpressionContext ctx) {
switchExpressionRuleContextStack.push(ctx);
try {
validateSwitchExpressionLabels(ctx);
@@ -1074,18 +1061,13 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
}
- Statement statement = configureAST(
- new SwitchStatement(
+ return configureAST(
+ new SwitchExpression(
this.visitExpressionInPar(ctx.expressionInPar()),
caseStatements,
defaultStatement != null ? defaultStatement : EmptyStatement.INSTANCE
),
ctx);
- statement = createBlockStatement(List.of(statement));
-
- MethodCallExpression immediateExecution = callX(closureX(null, statement), CALL_STR);
- immediateExecution.setImplicitThis(false);
- return immediateExecution;
} finally {
switchExpressionRuleContextStack.pop();
}
@@ -1131,25 +1113,24 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
boolean[] hasThrowHolder = new boolean[1];
codeBlock.visit(new CodeVisitorSupport() {
@Override
- public void visitReturnStatement(ReturnStatement statement) {
- if (isTrue(statement, IS_YIELD_STATEMENT)) {
- hasYieldHolder[0] = true;
- return;
- }
-
- super.visitReturnStatement(statement);
+ public void visitYieldStatement(final YieldStatement statement) {
+ hasYieldHolder[0] = true;
}
@Override
public void visitThrowStatement(ThrowStatement statement) {
hasThrowHolder[0] = true;
}
+
+ @Override
+ public void visitClosureExpression(final ClosureExpression expression) {
+ // yield inside a nested closure does not complete this arm
+ }
});
if (hasYieldHolder[0] || hasThrowHolder[0]) {
hasResultStmtHolder[0] = true;
}
-
}
Statement exprOrBlockStatement = statements.get(0);
@@ -1160,26 +1141,34 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
}
- if (!(exprOrBlockStatement instanceof ReturnStatement || exprOrBlockStatement instanceof ThrowStatement)) {
+ if (!(exprOrBlockStatement instanceof YieldStatement || exprOrBlockStatement instanceof ThrowStatement)) {
if (isArrow) {
- MethodCallExpression callClosure = callX(
- configureAST(
- closureX(null, exprOrBlockStatement),
- exprOrBlockStatement
- ), CALL_STR);
- callClosure.setImplicitThis(false);
- Expression resultExpr = exprOrBlockStatement instanceof ExpressionStatement
- ? ((ExpressionStatement) exprOrBlockStatement).getExpression()
- : callClosure;
-
- codeBlock = configureAST(
- createBlockStatement(configureAST(
- returnS(resultExpr),
- exprOrBlockStatement
- )),
- exprOrBlockStatement
- );
+ if (exprOrBlockStatement instanceof ExpressionStatement expressionStatement) {
+ codeBlock = configureAST(
+ createBlockStatement(configureAST(
+ yieldS(expressionStatement.getExpression()),
+ exprOrBlockStatement
+ )),
+ exprOrBlockStatement
+ );
+ } else if (!containsYieldOrThrow(exprOrBlockStatement)) {
+ throw createParsingFailedException("`yield` or `throw` is expected", exprOrBlockStatement);
+ } else {
+ codeBlock = configureAST(
+ createBlockStatement(exprOrBlockStatement),
+ exprOrBlockStatement
+ );
+ }
}
+ } else if (exprOrBlockStatement instanceof YieldStatement || exprOrBlockStatement instanceof ThrowStatement) {
+ codeBlock = configureAST(
+ createBlockStatement(exprOrBlockStatement),
+ exprOrBlockStatement
+ );
+ }
+
+ if (isArrow && org.codehaus.groovy.ast.tools.GeneralUtils.maybeFallsThrough(codeBlock)) {
+ throw createParsingFailedException("`yield` or `throw` is expected", exprOrBlockStatement);
}
switch (tuple.getV1().getType()) {
@@ -1189,16 +1178,12 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
for (int i = 0, n = tuple.getV2().size(); i < n; i += 1) {
Expression expr = tuple.getV2().get(i);
- statementList.add(
- configureAST(
- new CaseStatement(
- expr,
-
- // check whether processing the last label. if yes, block statement should be attached.
- (isLast && i == n - 1) ? codeBlock
- : EmptyStatement.INSTANCE
- ),
- firstLabelHolder.get(0)));
+ CaseStatement caseStatement = new CaseStatement(
+ expr,
+ (isLast && i == n - 1) ? codeBlock : EmptyStatement.INSTANCE
+ );
+ caseStatement.setArrow(isArrow);
+ statementList.add(configureAST(caseStatement, firstLabelHolder.get(0)));
}
break;
case DEFAULT:
@@ -1213,6 +1198,44 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return tuple(result, isArrowHolder[0], hasResultStmtHolder[0]);
}
+ /**
+ * True when a switch-expression frame is more recent than any closure or
+ * lambda frame. {@code return} in that region would leave the enclosing
+ * method rather than complete the expression (JEP 361).
+ */
+ private boolean isInsideSwitchExpression() {
+ for (var ctx : switchExpressionRuleContextStack) {
+ if (ctx instanceof SwitchExpressionContext) {
+ return true;
+ }
+ if (ctx instanceof ClosureContext || ctx instanceof StandardLambdaExpressionContext) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private static boolean containsYieldOrThrow(final Statement statement) {
+ boolean[] found = new boolean[1];
+ statement.visit(new CodeVisitorSupport() {
+ @Override
+ public void visitYieldStatement(final YieldStatement yieldStatement) {
+ found[0] = true;
+ }
+
+ @Override
+ public void visitThrowStatement(final ThrowStatement throwStatement) {
+ found[0] = true;
+ }
+
+ @Override
+ public void visitClosureExpression(final ClosureExpression expression) {
+ // do not look inside nested closures / lambdas
+ }
+ });
+ return found[0];
+ }
+
private void validateSwitchExpressionLabels(SwitchExpressionContext ctx) {
Map<String, List<SwitchExpressionLabelContext>> acMap =
ctx.switchBlockStatementExpressionGroup().stream()
@@ -5056,7 +5079,6 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
private static final String INTEGER_LITERAL_TEXT = "_INTEGER_LITERAL_TEXT";
private static final String FLOATING_POINT_LITERAL_TEXT = "_FLOATING_POINT_LITERAL_TEXT";
private static final String ENCLOSING_INSTANCE_EXPRESSION = "_ENCLOSING_INSTANCE_EXPRESSION";
- private static final String IS_YIELD_STATEMENT = "_IS_YIELD_STATEMENT";
private static final String PARAMETER_MODIFIER_MANAGER = "_PARAMETER_MODIFIER_MANAGER";
private static final String PARAMETER_CONTEXT = "_PARAMETER_CONTEXT";
private static final String IS_RECORD_GENERATED = "_IS_RECORD_GENERATED";
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java b/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java
index 71f9865667..58d22d9429 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassCodeExpressionTransformer.java
@@ -24,6 +24,7 @@ import org.codehaus.groovy.ast.expr.ClosureListExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ExpressionTransformer;
import org.codehaus.groovy.ast.expr.PropertyExpression;
+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.DoWhileStatement;
@@ -35,6 +36,7 @@ import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import java.util.Map;
@@ -179,6 +181,20 @@ public abstract class ClassCodeExpressionTransformer extends ClassCodeVisitorSup
stmt.setExpression(transform(stmt.getExpression()));
}
+ @Override
+ public void visitYieldStatement(final YieldStatement stmt) {
+ stmt.setExpression(transform(stmt.getExpression()));
+ }
+
+ @Override
+ public void visitSwitchExpression(final SwitchExpression expression) {
+ expression.setExpression(transform(expression.getExpression()));
+ for (CaseStatement caseStatement : expression.getCaseStatements()) {
+ caseStatement.visit(this);
+ }
+ expression.getDefaultStatement().visit(this);
+ }
+
@Override
public void visitWhileLoop(final WhileStatement stmt) {
visitStatementAnnotations(stmt);
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java b/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java
index c1fbdd1f98..82c0ccdde4 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java
@@ -38,6 +38,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.SyntaxException;
import org.codehaus.groovy.transform.ErrorCollecting;
@@ -406,6 +407,19 @@ public abstract class ClassCodeVisitorSupport extends CodeVisitorSupport impleme
super.visitSwitch(statement);
}
+ /**
+ * Visits a {@link YieldStatement}, invoking the statement hook before parent
+ * traversal (GROOVY-12255).
+ *
+ * @param statement the yield statement to visit
+ * @since 6.0.0
+ */
+ @Override
+ public void visitYieldStatement(final YieldStatement statement) {
+ visitStatement(statement);
+ super.visitYieldStatement(statement);
+ }
+
/**
* Visits a {@link SynchronizedStatement}, invoking the statement hook before parent traversal.
*
diff --git a/src/main/java/org/codehaus/groovy/ast/CodeVisitorSupport.java b/src/main/java/org/codehaus/groovy/ast/CodeVisitorSupport.java
index fc6f6992cc..3c68b88301 100644
--- a/src/main/java/org/codehaus/groovy/ast/CodeVisitorSupport.java
+++ b/src/main/java/org/codehaus/groovy/ast/CodeVisitorSupport.java
@@ -49,6 +49,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -72,6 +73,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.classgen.BytecodeExpression;
/**
@@ -313,6 +315,18 @@ public abstract class CodeVisitorSupport implements GroovyCodeVisitor {
public void visitEmptyStatement(final EmptyStatement statement) {
}
+ /**
+ * Visits a {@link YieldStatement}, traversing the yielded expression
+ * (GROOVY-12255).
+ *
+ * @param statement the yield statement
+ * @since 6.0.0
+ */
+ @Override
+ public void visitYieldStatement(final YieldStatement statement) {
+ statement.getExpression().visit(this);
+ }
+
/**
* Visits a {@link org.codehaus.groovy.ast.expr.MethodCallExpression}, traversing the object expression,
* method expression, and argument list.
@@ -369,6 +383,44 @@ public abstract class CodeVisitorSupport implements GroovyCodeVisitor {
expression.getFalseExpression().visit(this);
}
+ /**
+ * Visits a {@link SwitchExpression}, traversing the selector, case arms, and
+ * default arm (GROOVY-12255).
+ *
+ * @param expression the switch expression
+ * @since 6.0.0
+ */
+ @Override
+ public void visitSwitchExpression(final SwitchExpression expression) {
+ expression.getExpression().visit(this);
+ afterSwitchConditionExpressionVisited(expression);
+ for (CaseStatement caseStatement : expression.getCaseStatements()) {
+ caseStatement.visit(this);
+ }
+ afterSwitchCaseStatementsVisited(expression);
+ expression.getDefaultStatement().visit(this);
+ }
+
+ /**
+ * Hook method called after the switch-expression selector is visited, but
+ * before case arms (GROOVY-12255).
+ *
+ * @param expression the switch expression being visited
+ * @since 6.0.0
+ */
+ protected void afterSwitchConditionExpressionVisited(final SwitchExpression expression) {
+ }
+
+ /**
+ * Hook method called after all case arms are visited, but before the default
+ * arm (GROOVY-12255).
+ *
+ * @param expression the switch expression being visited
+ * @since 6.0.0
+ */
+ protected void afterSwitchCaseStatementsVisited(final SwitchExpression expression) {
+ }
+
/**
* Visits a {@link ElvisOperatorExpression}, treating it as a ternary expression.
*
diff --git a/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitor.java b/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitor.java
index c9861b22bc..46ddc42107 100644
--- a/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitor.java
+++ b/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitor.java
@@ -51,6 +51,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -74,6 +75,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.classgen.BytecodeExpression;
import java.util.List;
@@ -214,6 +216,19 @@ public interface GroovyCodeVisitor {
default void visitEmptyStatement(EmptyStatement statement) {
}
+ /**
+ * Visits a {@code yield} statement that produces the value of an enclosing
+ * {@link SwitchExpression} (GROOVY-12255). The default implementation
+ * visits the yielded expression so existing visitors keep traversing
+ * without a source change.
+ *
+ * @param statement the {@link YieldStatement} to process
+ * @since 6.0.0
+ */
+ default void visitYieldStatement(YieldStatement statement) {
+ visit(statement.getExpression());
+ }
+
/**
* Convenience method to visit a statement if it is not null.
* Delegates to the statement's visit method.
@@ -496,6 +511,22 @@ public interface GroovyCodeVisitor {
default void visitEmptyExpression(EmptyExpression expression) {
}
+ /**
+ * Visits a first-class switch expression (GROOVY-12255 / JEP 361). The
+ * default implementation visits the selector, each case, and the default
+ * arm so existing visitors keep traversing without a source change.
+ *
+ * @param expression the {@link SwitchExpression} to process
+ * @since 6.0.0
+ */
+ default void visitSwitchExpression(SwitchExpression expression) {
+ visit(expression.getExpression());
+ for (CaseStatement caseStatement : expression.getCaseStatements()) {
+ caseStatement.visit(this);
+ }
+ visit(expression.getDefaultStatement());
+ }
+
/**
* Convenience method to visit a list of expressions.
* Iterates through the list and calls the visit method on each non-null expression.
diff --git a/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitorAdapter.java b/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitorAdapter.java
index 081c3a9d97..96930dbe2b 100644
--- a/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitorAdapter.java
+++ b/src/main/java/org/codehaus/groovy/ast/GroovyCodeVisitorAdapter.java
@@ -50,6 +50,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -72,6 +73,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.classgen.BytecodeExpression;
/**
@@ -249,6 +251,17 @@ public class GroovyCodeVisitorAdapter implements GroovyCodeVisitor {
public void visitWhileLoop(WhileStatement statement) {
}
+ /**
+ * Visits a {@link YieldStatement}. Default implementation is no-op
+ * (GROOVY-12255).
+ *
+ * @param statement the yield statement
+ * @since 6.0.0
+ */
+ @Override
+ public void visitYieldStatement(YieldStatement statement) {
+ }
+
// Expressions:
/**
@@ -573,6 +586,17 @@ public class GroovyCodeVisitorAdapter implements GroovyCodeVisitor {
public void visitTernaryExpression(TernaryExpression expression) {
}
+ /**
+ * Visits a {@link SwitchExpression}. Default implementation is no-op
+ * (GROOVY-12255).
+ *
+ * @param expression the switch expression
+ * @since 6.0.0
+ */
+ @Override
+ public void visitSwitchExpression(SwitchExpression expression) {
+ }
+
/**
* Visits a {@link TupleExpression}. Default implementation is no-op.
*
diff --git a/src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java b/src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java
new file mode 100644
index 0000000000..4d1dda1972
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/ast/expr/SwitchExpression.java
@@ -0,0 +1,201 @@
+/*
+ * 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.CodeVisitorSupport;
+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.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.SwitchStatement;
+import org.codehaus.groovy.ast.stmt.ThrowStatement;
+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>
+ * This is a first-class expression: it is <em>not</em> desugared to a
+ * closure wrapping a {@link SwitchStatement} (GROOVY-12255).
+ *
+ * @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 + "]";
+ }
+
+ @Override
+ public Expression transformExpression(final ExpressionTransformer transformer) {
+ List<CaseStatement> transformedCases = new ArrayList<>(caseStatements.size());
+ for (CaseStatement caseStatement : caseStatements) {
+ CaseStatement copy = new CaseStatement(transformer.transform(caseStatement.getExpression()), caseStatement.getCode());
+ copy.setArrow(caseStatement.isArrow());
+ copy.setSourcePosition(caseStatement);
+ copy.copyNodeMetaData(caseStatement);
+ transformArmExpressions(copy.getCode(), transformer);
+ if (transformer instanceof GroovyCodeVisitor visitor) {
+ copy.getCode().visit(visitor);
+ }
+ transformedCases.add(copy);
+ }
+ transformArmExpressions(defaultStatement, transformer);
+ if (transformer instanceof GroovyCodeVisitor visitor) {
+ defaultStatement.visit(visitor);
+ }
+ SwitchExpression ret = new SwitchExpression(transformer.transform(expression), transformedCases, defaultStatement);
+ ret.setSourcePosition(this);
+ ret.copyNodeMetaData(this);
+ ret.setType(getType());
+ return ret;
+ }
+
+ @Override
+ public void visit(final GroovyCodeVisitor visitor) {
+ visitor.visitSwitchExpression(this);
+ }
+
+ private static void transformArmExpressions(final Statement statement, final ExpressionTransformer transformer) {
+ statement.visit(new CodeVisitorSupport() {
+ @Override
+ public void visitYieldStatement(final YieldStatement yieldStatement) {
+ yieldStatement.setExpression(transformer.transform(yieldStatement.getExpression()));
+ }
+
+ @Override
+ public void visitThrowStatement(final ThrowStatement throwStatement) {
+ throwStatement.setExpression(transformer.transform(throwStatement.getExpression()));
+ }
+
+ @Override
+ public void visitExpressionStatement(final ExpressionStatement expressionStatement) {
+ expressionStatement.setExpression(transformer.transform(expressionStatement.getExpression()));
+ }
+ });
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/ast/query/AstQuery.java b/src/main/java/org/codehaus/groovy/ast/query/AstQuery.java
index 6fc8e0400e..da66b2a67d 100644
--- a/src/main/java/org/codehaus/groovy/ast/query/AstQuery.java
+++ b/src/main/java/org/codehaus/groovy/ast/query/AstQuery.java
@@ -469,6 +469,8 @@ public final class AstQuery<T extends ASTNode> {
@Override public void visitSynchronizedStatement(org.codehaus.groovy.ast.stmt.SynchronizedStatement s) { enter(s, () -> super.visitSynchronizedStatement(s)); }
@Override public void visitCatchStatement(org.codehaus.groovy.ast.stmt.CatchStatement s) { enter(s, () -> super.visitCatchStatement(s)); }
@Override public void visitEmptyStatement(org.codehaus.groovy.ast.stmt.EmptyStatement s) { enter(s, () -> super.visitEmptyStatement(s)); }
+ @Override public void visitYieldStatement(org.codehaus.groovy.ast.stmt.YieldStatement s) { enter(s, () -> super.visitYieldStatement(s)); }
+ @Override public void visitSwitchExpression(org.codehaus.groovy.ast.expr.SwitchExpression e) { enter(e, () -> super.visitSwitchExpression(e)); }
// ---- expressions (sinks only; wrappers funnel into these) ----
diff --git a/src/main/java/org/codehaus/groovy/ast/stmt/CaseStatement.java b/src/main/java/org/codehaus/groovy/ast/stmt/CaseStatement.java
index c3bb5c9fa6..1f25d66a7c 100644
--- a/src/main/java/org/codehaus/groovy/ast/stmt/CaseStatement.java
+++ b/src/main/java/org/codehaus/groovy/ast/stmt/CaseStatement.java
@@ -35,6 +35,7 @@ public class CaseStatement extends Statement {
private Statement code;
private Expression expression;
+ private boolean arrow;
/**
* Constructs a case statement with the given expression pattern and code block.
@@ -83,6 +84,29 @@ public class CaseStatement extends Statement {
expression=e;
}
+ /**
+ * Indicates whether this case uses an arrow label ({@code case L ->}) rather
+ * than a colon label ({@code case L:}). Arrow labels do not fall through
+ * (GROOVY-12255 / JEP 361).
+ *
+ * @return {@code true} if this case was written with {@code ->}
+ * @since 6.0.0
+ */
+ public boolean isArrow() {
+ return arrow;
+ }
+
+ /**
+ * Marks this case as an arrow label ({@code case L ->}) or a colon label
+ * (GROOVY-12255).
+ *
+ * @param arrow {@code true} if this case was written with {@code ->}
+ * @since 6.0.0
+ */
+ public void setArrow(final boolean arrow) {
+ this.arrow = arrow;
+ }
+
@Override
public void visit(GroovyCodeVisitor visitor) {
visitor.visitCaseStatement(this);
@@ -90,6 +114,6 @@ public class CaseStatement extends Statement {
@Override
public String toString() {
- return super.toString() + "[expression: " + expression + "; code: " + code + "]";
+ return super.toString() + "[expression: " + expression + "; code: " + code + "; arrow: " + arrow + "]";
}
}
diff --git a/src/main/java/org/codehaus/groovy/ast/stmt/YieldStatement.java b/src/main/java/org/codehaus/groovy/ast/stmt/YieldStatement.java
new file mode 100644
index 0000000000..9b60c4bc67
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/ast/stmt/YieldStatement.java
@@ -0,0 +1,81 @@
+/*
+ * 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.stmt;
+
+import org.codehaus.groovy.ast.GroovyCodeVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
+
+/**
+ * Represents a {@code yield} statement that produces the value of an enclosing
+ * {@link SwitchExpression}. Unlike {@link ReturnStatement}, {@code yield} does
+ * not return from the enclosing method; it completes the switch expression and
+ * transfers the yielded value to the expression's join point (JEP 361 /
+ * GROOVY-12255).
+ *
+ * @see SwitchExpression
+ * @see ReturnStatement
+ * @since 6.0.0
+ */
+public class YieldStatement extends Statement {
+
+ private Expression expression;
+
+ /**
+ * Constructs a yield statement with the given result expression.
+ *
+ * @param expression the value produced for the enclosing switch expression
+ */
+ public YieldStatement(final Expression expression) {
+ setExpression(expression);
+ }
+
+ /**
+ * Returns the expression whose value becomes the switch-expression result.
+ *
+ * @return the yielded {@link Expression}
+ */
+ public Expression getExpression() {
+ return expression;
+ }
+
+ /**
+ * Sets the expression whose value becomes the switch-expression result.
+ *
+ * @param expression the yielded {@link Expression}
+ */
+ public void setExpression(final Expression expression) {
+ this.expression = expression;
+ }
+
+ @Override
+ public String getText() {
+ return "yield " + expression.getText();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "[expression:" + expression + "]";
+ }
+
+ @Override
+ public void visit(final GroovyCodeVisitor visitor) {
+ visitor.visitYieldStatement(this);
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 057c244440..1d6addc345 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -56,6 +56,7 @@ import org.codehaus.groovy.ast.expr.NotExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
@@ -76,6 +77,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.classgen.BytecodeExpression;
import org.codehaus.groovy.control.io.ReaderSource;
import org.codehaus.groovy.runtime.GeneratedClosure;
@@ -1636,6 +1638,16 @@ public class GeneralUtils {
public static Statement returnS(final Expression expr) {
return new ReturnStatement(expr);
}
+ /**
+ * Creates a yield statement for a switch expression (GROOVY-12255).
+ *
+ * @param expr the yielded expression
+ * @return the resulting YieldStatement
+ * @since 6.0.0
+ */
+ public static YieldStatement yieldS(final Expression expr) {
+ return new YieldStatement(expr);
+ }
/**
* Creates a null-guarded statement sequence.
*
@@ -1704,6 +1716,18 @@ public class GeneralUtils {
public static SwitchStatement switchS(final Expression expr, final List<CaseStatement> caseStatements, final Statement defaultStatement) {
return new SwitchStatement(expr, caseStatements, defaultStatement);
}
+ /**
+ * Creates a first-class switch expression (GROOVY-12255 / JEP 361).
+ *
+ * @param expr the selector expression
+ * @param caseStatements the case arms
+ * @param defaultStatement the default arm
+ * @return the resulting SwitchExpression
+ * @since 6.0.0
+ */
+ public static SwitchExpression switchX(final Expression expr, final List<CaseStatement> caseStatements, final Statement defaultStatement) {
+ return new SwitchExpression(expr, caseStatements, defaultStatement);
+ }
/**
* Creates a ternary expression.
*
@@ -2101,7 +2125,7 @@ public class GeneralUtils {
public static boolean maybeFallsThrough(final Statement statement) {
if (statement.isEmpty()) return true;
- if (statement instanceof ReturnStatement) {
+ if (statement instanceof ReturnStatement || statement instanceof YieldStatement) {
return false;
} else if (statement instanceof ThrowStatement) {
return false;
@@ -2213,7 +2237,8 @@ public class GeneralUtils {
return StatementFlow.breakFlow(breakStatement.getLabel());
} else if (statement instanceof ContinueStatement continueStatement) {
return StatementFlow.continueFlow(continueStatement.getLabel());
- } else if (statement instanceof ReturnStatement || statement instanceof ThrowStatement) {
+ } else if (statement instanceof ReturnStatement || statement instanceof ThrowStatement
+ || statement instanceof YieldStatement) {
return StatementFlow.ABRUPT;
} else if (statement instanceof BlockStatement block) {
StatementFlow flow = StatementFlow.FALL_THROUGH;
diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
index 3b83617bf5..faf367c82c 100644
--- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
+++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
@@ -72,6 +72,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -94,6 +95,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
import org.codehaus.groovy.classgen.asm.BytecodeVariable;
import org.codehaus.groovy.classgen.asm.CallSiteWriter;
@@ -980,6 +982,23 @@ public class AsmClassGenerator extends ClassGenerator {
doPostVisit(expression); // GROOVY-7473
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void visitSwitchExpression(final SwitchExpression expression) {
+ controller.getSwitchExpressionWriter().writeSwitchExpression(expression);
+ doPostVisit(expression);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void visitYieldStatement(final YieldStatement statement) {
+ controller.getSwitchExpressionWriter().writeYield(statement);
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/main/java/org/codehaus/groovy/classgen/FinalVariableAnalyzer.java b/src/main/java/org/codehaus/groovy/classgen/FinalVariableAnalyzer.java
index 92861c947e..3ebe382abd 100644
--- a/src/main/java/org/codehaus/groovy/classgen/FinalVariableAnalyzer.java
+++ b/src/main/java/org/codehaus/groovy/classgen/FinalVariableAnalyzer.java
@@ -42,6 +42,7 @@ import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
@@ -360,6 +361,65 @@ public class FinalVariableAnalyzer extends ClassCodeVisitorSupport {
/**
* {@inheritDoc}
*/
+ @Override
+ public void visitSwitchExpression(final org.codehaus.groovy.ast.expr.SwitchExpression expression) {
+ expression.getExpression().visit(this);
+ List<Statement> branches = new ArrayList<>(expression.getCaseStatements());
+ if (!(expression.getDefaultStatement() instanceof EmptyStatement)) {
+ branches.add(expression.getDefaultStatement());
+ }
+ List<Map<Variable, VariableState>> afterStates = new ArrayList<>();
+
+ int lastIndex = branches.size() - 1;
+ for (int i = 0; i <= lastIndex; i++) {
+ pushState();
+ boolean done = false;
+ boolean leavesExpression = false;
+ for (int j = i; !done; j++) {
+ Statement branch = branches.get(j);
+ Statement block = branch;
+ if (branch instanceof CaseStatement caseS) {
+ block = caseS.getCode();
+ caseS.getExpression().visit(this);
+ }
+ block.visit(this);
+ done = j == lastIndex || !fallsThrough(block);
+ if (done) {
+ // GROOVY-12255: yield completes the expression normally;
+ // only throw/return fail to reach the code after it
+ leavesExpression = completesWithoutReachingAfterExpression(block);
+ }
+ }
+ if (!leavesExpression) {
+ afterStates.add(getState());
+ }
+ popState();
+ }
+ if (afterStates.isEmpty()) {
+ return;
+ }
+
+ Map<Variable, VariableState> beforeState = getState();
+ Set<Variable> allVars = new HashSet<>(beforeState.keySet());
+ for (Map<Variable, VariableState> map : afterStates) {
+ allVars.addAll(map.keySet());
+ }
+ for (Variable var : allVars) {
+ VariableState beforeValue = beforeState.get(var);
+ if (beforeValue != null) {
+ final VariableState merged = afterStates.get(0).get(var);
+ if (merged != null) {
+ if (afterStates.stream().allMatch(state -> merged.equals(state.get(var)))) {
+ beforeState.put(var, merged);
+ } else {
+ VariableState different = beforeValue == VariableState.is_uninitialized ? VariableState.is_ambiguous : VariableState.is_var;
+ beforeState.put(var, different);
+ }
+ }
+ }
+ }
+ }
+
@Override
public void visitSwitch(SwitchStatement switchS) {
visitStatement(switchS);
@@ -489,11 +549,30 @@ public class FinalVariableAnalyzer extends ClassCodeVisitorSupport {
popState();
}
+ /**
+ * True when every path through {@code block} leaves the enclosing method
+ * (return) or throws, so assignments in that arm do not reach the code
+ * after a switch expression (GROOVY-12255). {@link YieldStatement}
+ * completes the expression normally and therefore does reach that code.
+ */
+ private boolean completesWithoutReachingAfterExpression(final Statement block) {
+ if (block instanceof ReturnStatement || block instanceof ThrowStatement) {
+ return true;
+ }
+ if (block instanceof YieldStatement) {
+ return false;
+ }
+ if (block instanceof BlockStatement bs && !bs.getStatements().isEmpty()) {
+ return completesWithoutReachingAfterExpression(DefaultGroovyMethods.last(bs.getStatements()));
+ }
+ return false;
+ }
+
/**
* @return true if the block's last statement is a return or throw
*/
private boolean returningBlock(Statement block) {
- if (block instanceof ReturnStatement || block instanceof ThrowStatement) {
+ if (block instanceof ReturnStatement || block instanceof ThrowStatement || block instanceof YieldStatement) {
return true;
}
if (!(block instanceof BlockStatement bs)) {
@@ -503,7 +582,7 @@ public class FinalVariableAnalyzer extends ClassCodeVisitorSupport {
return false;
}
Statement last = DefaultGroovyMethods.last(bs.getStatements());
- if (last instanceof ReturnStatement || last instanceof ThrowStatement) {
+ if (last instanceof ReturnStatement || last instanceof ThrowStatement || last instanceof YieldStatement) {
return true;
}
return false;
@@ -524,7 +603,7 @@ public class FinalVariableAnalyzer extends ClassCodeVisitorSupport {
return true;
}
Statement last = DefaultGroovyMethods.last(block.getStatements());
- boolean completesAbruptly = last instanceof ReturnStatement || last instanceof BreakStatement || last instanceof ThrowStatement || last instanceof ContinueStatement;
+ boolean completesAbruptly = last instanceof ReturnStatement || last instanceof BreakStatement || last instanceof ThrowStatement || last instanceof ContinueStatement || last instanceof YieldStatement;
return !completesAbruptly;
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java b/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java
index 330200c4c5..321ebc1b8a 100644
--- a/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java
+++ b/src/main/java/org/codehaus/groovy/classgen/VariableScopeVisitor.java
@@ -731,6 +731,16 @@ public class VariableScopeVisitor extends ClassCodeVisitorSupport {
popState();
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void visitSwitchExpression(final org.codehaus.groovy.ast.expr.SwitchExpression expression) {
+ pushState();
+ super.visitSwitchExpression(expression);
+ popState();
+ }
+
/**
* Visits a {@code while} loop with <em>partial</em> JEP 394 flow scoping
* for {@code instanceof} pattern variables (GROOVY-12242).
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java b/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
index 8d7a5b74c7..e7abc6543d 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/CompileStack.java
@@ -22,6 +22,7 @@ import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.Variable;
@@ -116,6 +117,12 @@ public class CompileStack {
private Map<String, Label> superBlockNamedLabels = new HashMap<>();
/** map containing named labels of current block */
private Map<String, Label> currentBlockNamedLabels = new HashMap<>();
+ /**
+ * Nested switch-expression join points (GROOVY-12255). {@code yield} jumps
+ * to the innermost {@link SwitchExpressionContext#endLabel} after applying
+ * intervening finally blocks.
+ */
+ private final Deque<SwitchExpressionContext> switchExpressions = new LinkedList<>();
/**
* list containing finally blocks
* <p>
@@ -689,6 +696,72 @@ public class CompileStack {
return breakLabel;
}
+ /**
+ * Pushes a switch-expression context so that {@code yield} can jump to the
+ * expression's join point (GROOVY-12255). The join label is installed as
+ * this frame's {@code breakLabel} (same as {@link #pushSwitch()}) so
+ * {@link #applyFinallyBlocks} only inlines recorders pushed after this
+ * call. A second {@link #pushState()} then snapshots that label; without
+ * it, a {@code try}/{@code synchronized} wrapping the whole expression
+ * would be treated as intervening.
+ *
+ * @param resultType the unified result type left on the operand stack at the join point
+ * @return the end label of the switch expression
+ * @since 6.0.0
+ */
+ public Label pushSwitchExpression(final ClassNode resultType) {
+ pushState();
+ Label endLabel = new Label();
+ breakLabel = endLabel;
+ pushState();
+ switchExpressions.push(new SwitchExpressionContext(endLabel, resultType));
+ return endLabel;
+ }
+
+ /**
+ * Returns the innermost switch-expression context, or {@code null} if the
+ * current method is not compiling a {@link SwitchExpression}
+ * (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public SwitchExpressionContext getSwitchExpressionContext() {
+ return switchExpressions.peek();
+ }
+
+ /**
+ * Pops the innermost switch-expression context and restores the previous
+ * compile-stack state (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public void popSwitchExpression() {
+ if (switchExpressions.isEmpty()) {
+ throw new GroovyBugError("Tried to pop a switch expression without a matching push.");
+ }
+ switchExpressions.pop();
+ pop();
+ pop();
+ }
+
+ /**
+ * Join-point information for a switch expression being compiled
+ * (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public static final class SwitchExpressionContext {
+ /** Label at which every completing arm joins, with the result on the stack. */
+ public final Label endLabel;
+ /** Type of the value left on the operand stack at {@link #endLabel}. */
+ public final ClassNode resultType;
+
+ SwitchExpressionContext(final Label endLabel, final ClassNode resultType) {
+ this.endLabel = endLabel;
+ this.resultType = resultType;
+ }
+ }
+
/**
* Creates a new break label and an element for the state stack so pop has
* to be called later.
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java b/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java
index 7ee7adef9d..383d66f11d 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/DelegatingController.java
@@ -197,6 +197,14 @@ public class DelegatingController extends WriterController {
return delegationController.getMethodReferenceExpressionWriter();
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public SwitchExpressionWriter getSwitchExpressionWriter() {
+ return delegationController.getSwitchExpressionWriter();
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
new file mode 100644
index 0000000000..ca1a957894
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/SwitchExpressionWriter.java
@@ -0,0 +1,578 @@
+/*
+ * 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;
+
+import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.PropertyExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.CaseStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
+import org.codehaus.groovy.classgen.AsmClassGenerator;
+import org.codehaus.groovy.classgen.asm.sc.StaticTypesTypeChooser;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.maybeFallsThrough;
+import static org.objectweb.asm.Opcodes.ALOAD;
+import static org.objectweb.asm.Opcodes.ATHROW;
+import static org.objectweb.asm.Opcodes.CHECKCAST;
+import static org.objectweb.asm.Opcodes.DUP;
+import static org.objectweb.asm.Opcodes.GOTO;
+import static org.objectweb.asm.Opcodes.IFEQ;
+import static org.objectweb.asm.Opcodes.ILOAD;
+import static org.objectweb.asm.Opcodes.ISTORE;
+import static org.objectweb.asm.Opcodes.INVOKESTATIC;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
+import static org.objectweb.asm.Opcodes.NEW;
+
+/**
+ * Emits JVM bytecode for a {@link SwitchExpression} (GROOVY-12255). Completing
+ * arms leave the result on the operand stack and jump to a shared join point —
+ * the same shape javac uses for JEP 361 switch expressions. There is no
+ * closure wrapper and no extra method.
+ * <p>
+ * When the selector type and case labels permit it, the writer emits
+ * {@code tableswitch} / {@code lookupswitch} (and the Java 7 string-switch
+ * two-switch form). Otherwise it falls back to Groovy's sequential
+ * {@code isCase} tests so Class, regex, Collection and Closure cases keep
+ * working.
+ *
+ * @since 6.0.0
+ */
+public class SwitchExpressionWriter {
+
+ private static final String ISE_INTERNAL_NAME = "java/lang/IllegalStateException";
+ private static final String ICCE_INTERNAL_NAME = "java/lang/IncompatibleClassChangeError";
+ private static final String STRING_BUILDER_INTERNAL_NAME = "java/lang/StringBuilder";
+
+ /** The controller coordinating all bytecode writers for the current class. */
+ protected final WriterController controller;
+
+ /**
+ * Creates a switch-expression writer with the given controller.
+ *
+ * @param controller the writer controller
+ */
+ public SwitchExpressionWriter(final WriterController controller) {
+ this.controller = controller;
+ }
+
+ /**
+ * Generates bytecode for a switch expression. The result is left on the
+ * operand stack with the expression's resolved type.
+ *
+ * @param expression the switch expression to compile
+ */
+ public void writeSwitchExpression(final SwitchExpression expression) {
+ AsmClassGenerator acg = controller.getAcg();
+ acg.onLineNumber(expression, "visitSwitchExpression");
+
+ WriterController effective = effectiveController();
+ CompileStack compileStack = effective.getCompileStack();
+ OperandStack operandStack = effective.getOperandStack();
+ MethodVisitor mv = effective.getMethodVisitor();
+
+ ClassNode resultType = resolveResultType(expression, effective);
+ Label endLabel = compileStack.pushSwitchExpression(resultType);
+
+ expression.getExpression().visit(acg);
+ ClassNode selectorType = operandStack.getTopOperand();
+ ClassNode storedSelectorType = ClassHelper.isPrimitiveType(selectorType)
+ ? ClassHelper.getWrapper(selectorType)
+ : selectorType;
+ operandStack.box();
+ int selectorIndex = compileStack.defineTemporaryVariable("switch", storedSelectorType, true);
+
+ boolean emitted = writeIntSwitch(expression, selectorIndex, storedSelectorType, resultType, endLabel)
+ || writeStringSwitch(expression, selectorIndex, storedSelectorType, resultType, endLabel)
+ || writeEnumSwitch(expression, selectorIndex, storedSelectorType, resultType, endLabel);
+ if (!emitted) {
+ writeIsCaseSwitch(expression, selectorIndex, endLabel);
+ }
+
+ mv.visitLabel(endLabel);
+ operandStack.push(resultType);
+
+ compileStack.removeVar(selectorIndex);
+ compileStack.popSwitchExpression();
+ }
+
+ /**
+ * Generates bytecode for a {@code yield} statement: evaluate the operand,
+ * cast it to the enclosing switch-expression result type, apply intervening
+ * finally blocks, and jump to the expression join point.
+ *
+ * @param statement the yield statement to compile
+ */
+ public void writeYield(final YieldStatement statement) {
+ WriterController effective = effectiveController();
+ CompileStack compileStack = effective.getCompileStack();
+ CompileStack.SwitchExpressionContext context = compileStack.getSwitchExpressionContext();
+ if (context == null) {
+ throw new GroovyBugError("yield outside of a switch expression");
+ }
+
+ effective.getAcg().onLineNumber(statement, "visitYieldStatement");
+ OperandStack operandStack = effective.getOperandStack();
+ MethodVisitor mv = effective.getMethodVisitor();
+ statement.getExpression().visit(effective.getAcg());
+ operandStack.doGroovyCast(context.resultType);
+ if (compileStack.hasBlockRecorder()) {
+ // stash the result so intervening finally / synchronized can run
+ // without seeing it on the operand stack (same shape as return)
+ int rv = compileStack.defineTemporaryVariable("$yield", context.resultType, true);
+ compileStack.applyFinallyBlocks(context.endLabel, true);
+ BytecodeHelper.load(mv, context.resultType, rv);
+ compileStack.removeVar(rv);
+ } else {
+ operandStack.remove(1);
+ }
+ mv.visitJumpInsn(GOTO, context.endLabel);
+ }
+
+ //--------------------------------------------------------------------------
+
+ private void writeIsCaseSwitch(final SwitchExpression expression, final int selectorIndex, final Label endLabel) {
+ WriterController effective = effectiveController();
+
+ List<CaseStatement> caseStatements = expression.getCaseStatements();
+ int caseCount = caseStatements.size();
+ Label[] bodyLabels = new Label[caseCount + 1];
+ for (int i = 0; i < caseCount; i += 1) {
+ bodyLabels[i] = new Label();
+ }
+
+ for (int i = 0; i < caseCount; i += 1) {
+ writeIsCaseArm(caseStatements.get(i), selectorIndex, bodyLabels[i], bodyLabels[i + 1]);
+ }
+
+ writeDefaultOrThrow(expression, selectorIndex, false);
+ }
+
+ private void writeIsCaseArm(final CaseStatement caseStatement, final int selectorIndex,
+ final Label thisLabel, final Label nextLabel) {
+ WriterController effective = effectiveController();
+ MethodVisitor mv = effective.getMethodVisitor();
+ OperandStack operandStack = effective.getOperandStack();
+ AsmClassGenerator acg = effective.getAcg();
+
+ acg.onLineNumber(caseStatement, "visitCaseStatement");
+
+ mv.visitVarInsn(ALOAD, selectorIndex);
+ caseStatement.getExpression().visit(acg);
+ operandStack.box();
+ effective.getBinaryExpressionHelper().getIsCaseMethod().call(mv);
+ operandStack.replace(ClassHelper.boolean_TYPE);
+
+ Label miss = operandStack.jump(IFEQ);
+
+ mv.visitLabel(thisLabel);
+ caseStatement.getCode().visit(acg);
+
+ if (nextLabel != null && maybeFallsThrough(caseStatement.getCode())) {
+ mv.visitJumpInsn(GOTO, nextLabel);
+ }
+
+ mv.visitLabel(miss);
+ }
+
+ private void writeDefaultOrThrow(final SwitchExpression expression, final int selectorIndex, final boolean completeEnum) {
+ WriterController effective = effectiveController();
+ Statement defaultStatement = expression.getDefaultStatement();
+ if (defaultStatement != null && !defaultStatement.isEmpty()) {
+ defaultStatement.visit(effective.getAcg());
+ return;
+ }
+ if (completeEnum) {
+ throwIncompatibleClassChangeError(effective.getMethodVisitor());
+ } else {
+ throwIllegalState(effective.getMethodVisitor(), selectorIndex);
+ }
+ }
+
+ private boolean writeIntSwitch(final SwitchExpression expression, final int selectorIndex,
+ final ClassNode storedSelectorType, final ClassNode resultType, final Label endLabel) {
+ if (!isStaticCompilation()) return false;
+ if (!isIntegralType(storedSelectorType) && !isIntegralWrapper(storedSelectorType)) return false;
+
+ List<CaseStatement> caseStatements = expression.getCaseStatements();
+ Map<Integer, Label> keyToBody = new TreeMap<>();
+ Label currentGroupBody = null;
+ for (CaseStatement caseStatement : caseStatements) {
+ Integer key = intConstant(caseStatement.getExpression());
+ if (key == null) return false;
+ if (!caseStatement.getCode().isEmpty() || caseStatement.isArrow()) {
+ currentGroupBody = new Label();
+ }
+ if (currentGroupBody == null) {
+ currentGroupBody = new Label();
+ }
+ if (keyToBody.put(key, currentGroupBody) != null) {
+ return false; // duplicate case value
+ }
+ }
+ if (keyToBody.isEmpty()) return false;
+
+ WriterController effective = effectiveController();
+ MethodVisitor mv = effective.getMethodVisitor();
+ OperandStack operandStack = effective.getOperandStack();
+ AsmClassGenerator acg = effective.getAcg();
+
+ mv.visitVarInsn(ALOAD, selectorIndex);
+ operandStack.push(storedSelectorType);
+ operandStack.doGroovyCast(ClassHelper.int_TYPE);
+ int intSelector = effective.getCompileStack().defineTemporaryVariable("$switchInt", ClassHelper.int_TYPE, true);
+
+ Label defaultLabel = new Label();
+ emitIntSwitch(mv, keyToBody, defaultLabel, intSelector);
+
+ // emit case bodies in source order; several keys may share a label
+ Set<Label> emitted = new HashSet<>();
+ for (CaseStatement caseStatement : caseStatements) {
+ Integer key = intConstant(caseStatement.getExpression());
+ Label body = keyToBody.get(key);
+ if (emitted.add(body)) {
+ mv.visitLabel(body);
+ }
+ if (!caseStatement.getCode().isEmpty()) {
+ caseStatement.getCode().visit(acg);
+ }
+ }
+
+ mv.visitLabel(defaultLabel);
+ writeDefaultOrThrow(expression, selectorIndex, false);
+
+ effective.getCompileStack().removeVar(intSelector);
+ return true;
+ }
+
+ private static void emitIntSwitch(final MethodVisitor mv, final Map<Integer, Label> keyToBody,
+ final Label defaultLabel, final int intSelector) {
+ mv.visitVarInsn(ILOAD, intSelector);
+ int[] keys = keyToBody.keySet().stream().mapToInt(Integer::intValue).toArray();
+ Label[] labels = keyToBody.values().toArray(Label[]::new);
+ int min = keys[0];
+ int max = keys[keys.length - 1];
+ long span = (long) max - (long) min + 1L;
+ // same size heuristic javac uses: tableswitch if it is no larger than lookupswitch
+ long tableSize = 12L + 4L * span;
+ long lookupSize = 8L + 8L * keys.length;
+ if (tableSize <= lookupSize) {
+ Label[] table = new Label[(int) span];
+ java.util.Arrays.fill(table, defaultLabel);
+ for (int i = 0; i < keys.length; i += 1) {
+ table[keys[i] - min] = labels[i];
+ }
+ mv.visitTableSwitchInsn(min, max, defaultLabel, table);
+ } else {
+ mv.visitLookupSwitchInsn(defaultLabel, keys, labels);
+ }
+ }
+
+ private boolean writeStringSwitch(final SwitchExpression expression, final int selectorIndex,
+ final ClassNode storedSelectorType, final ClassNode resultType, final Label endLabel) {
+ if (!isStaticCompilation()) return false;
+ if (!ClassHelper.isStringType(storedSelectorType)) {
+ return false;
+ }
+
+ List<CaseStatement> caseStatements = expression.getCaseStatements();
+ Map<String, Label> stringToBody = new LinkedHashMap<>();
+ Label currentGroupBody = null;
+ for (CaseStatement caseStatement : caseStatements) {
+ String key = stringConstant(caseStatement.getExpression());
+ if (key == null) return false;
+ if (!caseStatement.getCode().isEmpty() || caseStatement.isArrow()) {
+ currentGroupBody = new Label();
+ }
+ if (currentGroupBody == null) {
+ currentGroupBody = new Label();
+ }
+ if (stringToBody.put(key, currentGroupBody) != null) {
+ return false;
+ }
+ }
+ if (stringToBody.isEmpty()) return false;
+
+ WriterController effective = effectiveController();
+ MethodVisitor mv = effective.getMethodVisitor();
+ AsmClassGenerator acg = effective.getAcg();
+
+ // Java string-switch: switch(hashCode) + equals, then tableswitch on case index
+ Map<Integer, List<String>> byHash = new TreeMap<>();
+ List<String> ordered = new ArrayList<>(stringToBody.keySet());
+ for (String s : ordered) {
+ byHash.computeIfAbsent(s.hashCode(), h -> new ArrayList<>()).add(s);
+ }
+
+ Label defaultLabel = new Label();
+ Label secondSwitch = new Label();
+ int caseIndexLocal = effective.getCompileStack().defineTemporaryVariable("$switchCase", ClassHelper.int_TYPE, false);
+
+ requireNonNullSelector(mv, selectorIndex);
+ mv.visitTypeInsn(CHECKCAST, "java/lang/String");
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "hashCode", "()I", false);
+
+ int[] hashes = byHash.keySet().stream().mapToInt(Integer::intValue).toArray();
+ Label[] hashLabels = new Label[hashes.length];
+ for (int i = 0; i < hashes.length; i += 1) {
+ hashLabels[i] = new Label();
+ }
+ mv.visitLookupSwitchInsn(defaultLabel, hashes, hashLabels);
+
+ for (int i = 0; i < hashes.length; i += 1) {
+ mv.visitLabel(hashLabels[i]);
+ List<String> group = byHash.get(hashes[i]);
+ for (String s : group) {
+ mv.visitVarInsn(ALOAD, selectorIndex);
+ mv.visitLdcInsn(s);
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
+ Label next = new Label();
+ mv.visitJumpInsn(IFEQ, next);
+ mv.visitLdcInsn(ordered.indexOf(s));
+ mv.visitVarInsn(ISTORE, caseIndexLocal);
+ mv.visitJumpInsn(GOTO, secondSwitch);
+ mv.visitLabel(next);
+ }
+ mv.visitJumpInsn(GOTO, defaultLabel);
+ }
+
+ mv.visitLabel(secondSwitch);
+ Label[] bodyByIndex = new Label[ordered.size()];
+ for (int i = 0; i < ordered.size(); i += 1) {
+ bodyByIndex[i] = stringToBody.get(ordered.get(i));
+ }
+ mv.visitVarInsn(ILOAD, caseIndexLocal);
+ mv.visitTableSwitchInsn(0, ordered.size() - 1, defaultLabel, bodyByIndex);
+
+ Set<Label> emitted = new HashSet<>();
+ for (CaseStatement caseStatement : caseStatements) {
+ String key = stringConstant(caseStatement.getExpression());
+ Label body = stringToBody.get(key);
+ if (emitted.add(body)) {
+ mv.visitLabel(body);
+ }
+ if (!caseStatement.getCode().isEmpty()) {
+ caseStatement.getCode().visit(acg);
+ }
+ }
+
+ mv.visitLabel(defaultLabel);
+ writeDefaultOrThrow(expression, selectorIndex, false);
+
+ effective.getCompileStack().removeVar(caseIndexLocal);
+ return true;
+ }
+
+ /**
+ * Switches on {@code Enum.ordinal()} directly. Unlike javac's
+ * {@code $SwitchMap}, inserting a constant in the middle of a separately
+ * compiled enum can retarget arms; the implicit default only detects a
+ * newly added unmatched constant ({@code IncompatibleClassChangeError}).
+ */
+ private boolean writeEnumSwitch(final SwitchExpression expression, final int selectorIndex,
+ final ClassNode storedSelectorType, final ClassNode resultType, final Label endLabel) {
+ if (!isStaticCompilation()) return false;
+ ClassNode enumType = unwrapEnumType(storedSelectorType);
+ if (enumType == null || !enumType.isEnum()) return false;
+
+ List<CaseStatement> caseStatements = expression.getCaseStatements();
+ Map<Integer, Label> ordinalToBody = new TreeMap<>();
+ Label currentGroupBody = null;
+ int covered = 0;
+ for (CaseStatement caseStatement : caseStatements) {
+ Integer ordinal = enumOrdinal(caseStatement.getExpression(), enumType);
+ if (ordinal == null) return false;
+ if (!caseStatement.getCode().isEmpty() || caseStatement.isArrow()) {
+ currentGroupBody = new Label();
+ }
+ if (currentGroupBody == null) {
+ currentGroupBody = new Label();
+ }
+ if (ordinalToBody.put(ordinal, currentGroupBody) != null) {
+ return false;
+ }
+ covered += 1;
+ }
+ if (ordinalToBody.isEmpty()) return false;
+
+ WriterController effective = effectiveController();
+ MethodVisitor mv = effective.getMethodVisitor();
+ AsmClassGenerator acg = effective.getAcg();
+
+ requireNonNullSelector(mv, selectorIndex);
+ mv.visitTypeInsn(CHECKCAST, "java/lang/Enum");
+ mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Enum", "ordinal", "()I", false);
+ effective.getOperandStack().push(ClassHelper.int_TYPE);
+ int ordinalLocal = effective.getCompileStack().defineTemporaryVariable("$switchOrd", ClassHelper.int_TYPE, true);
+
+ Label defaultLabel = new Label();
+ emitIntSwitch(mv, ordinalToBody, defaultLabel, ordinalLocal);
+
+ Set<Label> emitted = new HashSet<>();
+ for (CaseStatement caseStatement : caseStatements) {
+ Integer ordinal = enumOrdinal(caseStatement.getExpression(), enumType);
+ Label body = ordinalToBody.get(ordinal);
+ if (emitted.add(body)) {
+ mv.visitLabel(body);
+ }
+ if (!caseStatement.getCode().isEmpty()) {
+ caseStatement.getCode().visit(acg);
+ }
+ }
+
+ mv.visitLabel(defaultLabel);
+ boolean complete = enumType.isResolved() && covered == enumType.getTypeClass().getEnumConstants().length
+ && (expression.getDefaultStatement() == null || expression.getDefaultStatement().isEmpty());
+ writeDefaultOrThrow(expression, selectorIndex, complete);
+
+ effective.getCompileStack().removeVar(ordinalLocal);
+ return true;
+ }
+
+ private static void requireNonNullSelector(final MethodVisitor mv, final int selectorIndex) {
+ mv.visitVarInsn(ALOAD, selectorIndex);
+ mv.visitMethodInsn(INVOKESTATIC, "java/util/Objects", "requireNonNull",
+ "(Ljava/lang/Object;)Ljava/lang/Object;", false);
+ }
+
+ private static void throwIllegalState(final MethodVisitor mv, final int selectorIndex) {
+ mv.visitTypeInsn(NEW, ISE_INTERNAL_NAME);
+ mv.visitInsn(DUP);
+ mv.visitTypeInsn(NEW, STRING_BUILDER_INTERNAL_NAME);
+ mv.visitInsn(DUP);
+ mv.visitLdcInsn("the switch expression does not cover the value ");
+ mv.visitMethodInsn(INVOKESPECIAL, STRING_BUILDER_INTERNAL_NAME, "<init>", "(Ljava/lang/String;)V", false);
+ mv.visitVarInsn(ALOAD, selectorIndex);
+ mv.visitMethodInsn(INVOKEVIRTUAL, STRING_BUILDER_INTERNAL_NAME, "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
+ mv.visitMethodInsn(INVOKEVIRTUAL, STRING_BUILDER_INTERNAL_NAME, "toString", "()Ljava/lang/String;", false);
+ mv.visitMethodInsn(INVOKESPECIAL, ISE_INTERNAL_NAME, "<init>", "(Ljava/lang/String;)V", false);
+ mv.visitInsn(ATHROW);
+ }
+
+ private static void throwIncompatibleClassChangeError(final MethodVisitor mv) {
+ mv.visitTypeInsn(NEW, ICCE_INTERNAL_NAME);
+ mv.visitInsn(DUP);
+ mv.visitLdcInsn("enum constant added after this switch expression was compiled");
+ mv.visitMethodInsn(INVOKESPECIAL, ICCE_INTERNAL_NAME, "<init>", "(Ljava/lang/String;)V", false);
+ mv.visitInsn(ATHROW);
+ }
+
+ private ClassNode resolveResultType(final SwitchExpression expression, final WriterController effective) {
+ ClassNode type = effective.getTypeChooser().resolveType(expression, effective.getClassNode());
+ if (type == null || ClassHelper.isDynamicTyped(type) || ClassHelper.isPrimitiveVoid(type)) {
+ type = ClassHelper.OBJECT_TYPE;
+ }
+ return type;
+ }
+
+ private WriterController effectiveController() {
+ WriterController fromAcg = controller.getAcg() != null ? controller.getAcg().getController() : null;
+ return fromAcg != null ? fromAcg : controller;
+ }
+
+ private boolean isStaticCompilation() {
+ return effectiveController().getTypeChooser() instanceof StaticTypesTypeChooser;
+ }
+
+ private static boolean isIntegralType(final ClassNode type) {
+ return ClassHelper.isPrimitiveInt(type) || ClassHelper.isPrimitiveByte(type)
+ || ClassHelper.isPrimitiveShort(type) || ClassHelper.isPrimitiveChar(type);
+ }
+
+ private static boolean isIntegralWrapper(final ClassNode type) {
+ return ClassHelper.isWrapperInteger(type) || ClassHelper.isWrapperByte(type)
+ || ClassHelper.isWrapperShort(type) || ClassHelper.isWrapperCharacter(type);
+ }
+
+ private static Integer intConstant(final Expression expression) {
+ if (!(expression instanceof ConstantExpression constant)) return null;
+ Object value = constant.getValue();
+ if (value instanceof Integer || value instanceof Byte || value instanceof Short) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof Character) {
+ return (int) (Character) value;
+ }
+ return null;
+ }
+
+ private static String stringConstant(final Expression expression) {
+ if (expression instanceof ConstantExpression constant && constant.getValue() instanceof String s) {
+ return s;
+ }
+ return null;
+ }
+
+ private static ClassNode unwrapEnumType(final ClassNode type) {
+ if (type == null) return null;
+ if (type.isEnum()) return type;
+ return type.redirect().isEnum() ? type.redirect() : null;
+ }
+
+ private static Integer enumOrdinal(final Expression expression, final ClassNode enumType) {
+ String name = enumConstantName(expression, enumType);
+ if (name == null) return null;
+ if (!enumType.isResolved()) return null;
+ try {
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Enum<?> constant = Enum.valueOf((Class) enumType.getTypeClass(), name);
+ return constant.ordinal();
+ } catch (RuntimeException ignored) {
+ return null;
+ }
+ }
+
+ private static String enumConstantName(final Expression expression, final ClassNode enumType) {
+ if (expression instanceof PropertyExpression property
+ && property.getObjectExpression() instanceof ClassExpression classExpression
+ && classExpression.getType().equals(enumType)
+ && property.getProperty() instanceof ConstantExpression name) {
+ return name.getText();
+ }
+ if (expression instanceof VariableExpression variable) {
+ FieldNode field = enumType.getField(variable.getName());
+ if (field != null && field.isEnum()) {
+ return variable.getName();
+ }
+ }
+ if (expression instanceof ConstantExpression constant && constant.getValue() instanceof Enum<?> e
+ && e.getDeclaringClass() == enumType.getTypeClass()) {
+ return e.name();
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
index 757c425cec..b5f070f254 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
@@ -95,6 +95,7 @@ public class WriterController {
private List<String> superMethodNames = new ArrayList<>();
private MethodPointerExpressionWriter methodPointerExpressionWriter;
private MethodReferenceExpressionWriter methodReferenceExpressionWriter;
+ private SwitchExpressionWriter switchExpressionWriter;
/**
* Initializes this controller with compilation context and ASM infrastructure.
@@ -160,6 +161,7 @@ public class WriterController {
this.lambdaWriter = new LambdaWriter(this);
this.methodPointerExpressionWriter = new MethodPointerExpressionWriter(this);
this.methodReferenceExpressionWriter = new MethodReferenceExpressionWriter(this);
+ this.switchExpressionWriter = new SwitchExpressionWriter(this);
this.internalBaseClassName = BytecodeHelper.getClassInternalName(cn.getSuperClass());
this.acg = asmClassGenerator;
this.context = gcon;
@@ -416,6 +418,16 @@ public class WriterController {
return methodReferenceExpressionWriter;
}
+ /**
+ * Returns the SwitchExpressionWriter for compiling first-class switch
+ * expressions (GROOVY-12255 / JEP 361).
+ *
+ * @since 6.0.0
+ */
+ public SwitchExpressionWriter getSwitchExpressionWriter() {
+ return switchExpressionWriter;
+ }
+
//--------------------------------------------------------------------------
/**
diff --git a/src/main/java/org/codehaus/groovy/control/LabelVerifier.java b/src/main/java/org/codehaus/groovy/control/LabelVerifier.java
index 2666927165..d1331d9943 100644
--- a/src/main/java/org/codehaus/groovy/control/LabelVerifier.java
+++ b/src/main/java/org/codehaus/groovy/control/LabelVerifier.java
@@ -37,7 +37,13 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.LambdaExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
+import java.util.ArrayDeque;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -50,6 +56,8 @@ import java.util.Set;
public class LabelVerifier extends PrePostStatementVisitor {
boolean inIf, inLoop, inSwitch;
+ private int closureDepth;
+ private final Deque<Integer> switchExpressionClosureDepths = new ArrayDeque<>();
private final SourceUnit source;
private Set<String> availableLabels;
private Map<String, Statement> undefinedLabels;
@@ -212,6 +220,57 @@ public class LabelVerifier extends PrePostStatementVisitor {
postVisitStatement(switchStatement);
}
+ /**
+ * Verifies labels and {@code yield} inside a switch expression
+ * (GROOVY-12255).
+ *
+ * @param expression the switch expression to inspect
+ * @since 6.0.0
+ */
+ @Override
+ public void visitSwitchExpression(final SwitchExpression expression) {
+ expression.getExpression().visit(this);
+ switchExpressionClosureDepths.push(closureDepth);
+ try {
+ expression.getCaseStatements().forEach(this::visit);
+ expression.getDefaultStatement().visit(this);
+ } finally {
+ switchExpressionClosureDepths.pop();
+ }
+ }
+
+ /**
+ * Verifies that a {@code yield} statement targets the innermost switch
+ * expression and does not cross a closure or lambda that was entered
+ * <em>after</em> that switch (GROOVY-12255 / JEP 361). A switch expression
+ * that itself lives inside a closure (for example a GINQ query) is allowed.
+ *
+ * @param statement the yield statement to inspect
+ * @since 6.0.0
+ */
+ @Override
+ public void visitYieldStatement(final YieldStatement statement) {
+ visitStatement(statement);
+ if (switchExpressionClosureDepths.isEmpty()) {
+ addError("the yield statement is only allowed inside a switch expression", statement);
+ } else if (closureDepth > switchExpressionClosureDepths.peek()) {
+ addError("yield cannot jump through a closure or lambda", statement);
+ }
+ super.visitYieldStatement(statement);
+ }
+
+ @Override
+ public void visitClosureExpression(final ClosureExpression expression) {
+ closureDepth += 1;
+ super.visitClosureExpression(expression);
+ closureDepth -= 1;
+ }
+
+ @Override
+ public void visitLambdaExpression(final LambdaExpression expression) {
+ visitClosureExpression(expression);
+ }
+
/**
* Verifies that a {@code break} statement appears in a valid context.
*
diff --git a/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java b/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java
index fa3eacadee..e72c4836b5 100644
--- a/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java
+++ b/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java
@@ -60,6 +60,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -83,6 +84,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
import org.codehaus.groovy.ast.stmt.ThrowStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.classgen.BytecodeExpression;
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationFailedException;
@@ -1775,6 +1777,35 @@ public class SecureASTCustomizer extends CompilationCustomizer {
statement.getDefaultStatement().visit(this);
}
+ /**
+ * Validates a switch expression and then visits its selector and arms
+ * (GROOVY-12255).
+ *
+ * @param expression the switch expression to visit
+ * @since 6.0.0
+ */
+ @Override
+ public void visitSwitchExpression(final SwitchExpression expression) {
+ assertExpressionAuthorized(expression);
+ expression.getExpression().visit(this);
+ for (CaseStatement caseStatement : expression.getCaseStatements()) {
+ caseStatement.visit(this);
+ }
+ expression.getDefaultStatement().visit(this);
+ }
+
+ /**
+ * Validates a yield statement and then visits its operand (GROOVY-12255).
+ *
+ * @param statement the yield statement to visit
+ * @since 6.0.0
+ */
+ @Override
+ public void visitYieldStatement(final YieldStatement statement) {
+ assertStatementAuthorized(statement);
+ statement.getExpression().visit(this);
+ }
+
/**
* Validates a case statement and then visits its condition and body.
*
diff --git a/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java b/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java
index 839583a6a5..46e612415a 100644
--- a/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/StaticTypesTransformation.java
@@ -100,7 +100,10 @@ public class StaticTypesTransformation implements ASTTransformation, Compilation
@Override
public Expression transform(final Expression expression) {
- if (expression instanceof ClosureExpression) { // switch expressions are desugared to closures
+ if (expression instanceof org.codehaus.groovy.ast.expr.SwitchExpression) {
+ return expression.transformExpression(this);
+ }
+ if (expression instanceof ClosureExpression) {
expression.visit(this);
return expression;
}
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/EnumTypeCheckingExtension.java b/src/main/java/org/codehaus/groovy/transform/stc/EnumTypeCheckingExtension.java
index 87e4ddefb7..32d01e8659 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/EnumTypeCheckingExtension.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/EnumTypeCheckingExtension.java
@@ -20,6 +20,7 @@ package org.codehaus.groovy.transform.stc;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.SWITCH_CONDITION_EXPRESSION_TYPE;
@@ -41,13 +42,14 @@ public class EnumTypeCheckingExtension extends TypeCheckingExtension {
}
/**
- * Resolves enum constants referenced inside switch case labels.
+ * Resolves enum constants referenced inside switch case labels, including
+ * first-class switch expressions (GROOVY-12255).
*/
@Override
public boolean handleUnresolvedVariableExpression(final VariableExpression vexp) {
- var switchStatement = this.typeCheckingVisitor.typeCheckingContext.getEnclosingSwitchStatement();
- if (switchStatement != null) {
- ClassNode type = switchStatement.getExpression().getNodeMetaData(StaticTypesMarker.TYPE);
+ Expression selector = selectorOfEnclosingSwitch();
+ if (selector != null) {
+ ClassNode type = selector.getNodeMetaData(StaticTypesMarker.TYPE);
if (type != null && type.isEnum()) {
FieldNode fieldNode = type.redirect().getField(vexp.getName());
if (fieldNode != null && fieldNode.isEnum()) {
@@ -58,4 +60,8 @@ public class EnumTypeCheckingExtension extends TypeCheckingExtension {
}
return false;
}
+
+ private Expression selectorOfEnclosingSwitch() {
+ return typeCheckingVisitor.typeCheckingContext.getEnclosingSwitchSelector();
+ }
}
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
index 18dc1c9897..8300f8ca12 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -86,6 +86,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.SpreadMapExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.expr.TernaryExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.UnaryMinusExpression;
@@ -105,6 +106,7 @@ import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.ast.stmt.TryCatchStatement;
import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.ast.stmt.YieldStatement;
import org.codehaus.groovy.ast.tools.GeneralUtils;
import org.codehaus.groovy.ast.tools.GenericsUtils;
import org.codehaus.groovy.ast.tools.WideningCategories;
@@ -4890,6 +4892,125 @@ trying: for (ClassNode[] signature : signatures) {
}
}
+ /**
+ * 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) {
+ typeCheckingContext.pushEnclosingSwitchExpression(expression);
+ try {
+ Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
+ try {
+ super.visitSwitchExpression(expression);
+ } finally {
+ popAssignmentTracking(oldTracker);
+ }
+
+ List<ClassNode> yieldTypes = typeCheckingContext.getEnclosingSwitchExpressionYieldTypes();
+ ClassNode resultType;
+ if (yieldTypes.isEmpty()) {
+ resultType = OBJECT_TYPE;
+ } else {
+ resultType = yieldTypes.get(0);
+ for (int i = 1; i < yieldTypes.size(); i += 1) {
+ resultType = lowestUpperBound(resultType, yieldTypes.get(i));
+ }
+ resultType = wrapTypeIfNecessary(checkForTargetType(expression, resultType));
+ }
+ storeType(expression, resultType);
+ expression.setType(resultType);
+
+ checkSwitchExpressionExhaustiveness(expression);
+ } finally {
+ typeCheckingContext.popTemporaryTypeInfo();
+ typeCheckingContext.popEnclosingSwitchExpression();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void afterSwitchConditionExpressionVisited(final SwitchExpression expression) {
+ typeCheckingContext.pushTemporaryTypeInfo();
+ Expression conditionExpression = expression.getExpression();
+ conditionExpression.putNodeMetaData(TYPE, getType(conditionExpression));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void afterSwitchCaseStatementsVisited(final SwitchExpression expression) {
+ if (!expression.getDefaultStatement().isEmpty()) {
+ Expression selectable = expression.getExpression();
+ optInstanceOfTypeInfo(selectable, selectable.getNodeMetaData(TYPE));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void visitYieldStatement(final YieldStatement statement) {
+ super.visitYieldStatement(statement);
+ if (typeCheckingContext.getEnclosingSwitchExpression() != null) {
+ typeCheckingContext.getEnclosingSwitchExpressionYieldTypes().add(getType(statement.getExpression()));
+ }
+ }
+
+ private Expression selectableOfEnclosingSwitch() {
+ return typeCheckingContext.getEnclosingSwitchSelector();
+ }
+
+ private void checkSwitchExpressionExhaustiveness(final SwitchExpression expression) {
+ if (expression.getDefaultStatement() != null && !expression.getDefaultStatement().isEmpty()) {
+ return;
+ }
+ Expression selector = expression.getExpression();
+ ClassNode selectorType = getType(selector);
+ if (selectorType != null && selectorType.isEnum() && coversAllEnumConstants(expression, selectorType)) {
+ return;
+ }
+ addError("the switch expression does not cover all possible input values", expression);
+ }
+
+ private boolean coversAllEnumConstants(final SwitchExpression expression, final ClassNode enumType) {
+ Set<String> remaining = new LinkedHashSet<>();
+ for (FieldNode field : enumType.redirect().getFields()) {
+ if (field.isEnum()) {
+ remaining.add(field.getName());
+ }
+ }
+ if (remaining.isEmpty() && enumType.isResolved()) {
+ Object[] constants = enumType.getTypeClass().getEnumConstants();
+ if (constants != null) {
+ for (Object constant : constants) {
+ remaining.add(((Enum<?>) constant).name());
+ }
+ }
+ }
+ if (remaining.isEmpty()) {
+ return false;
+ }
+ for (CaseStatement caseStatement : expression.getCaseStatements()) {
+ Expression caseExpr = caseStatement.getExpression();
+ if (caseExpr instanceof VariableExpression variable) {
+ remaining.remove(variable.getName());
+ } else if (caseExpr instanceof PropertyExpression property
+ && property.getObjectExpression() instanceof ClassExpression owner
+ && owner.getType().equals(enumType)
+ && property.getProperty() instanceof ConstantExpression name) {
+ remaining.remove(name.getText());
+ }
+ }
+ return remaining.isEmpty();
+ }
+
/** {@inheritDoc} */
@Override
protected void afterSwitchConditionExpressionVisited(final SwitchStatement statement) {
@@ -4911,7 +5032,11 @@ trying: for (ClassNode[] signature : signatures) {
/** {@inheritDoc} */
@Override
public void visitCaseStatement(final CaseStatement statement) {
- Expression selectable = typeCheckingContext.getEnclosingSwitchStatement().getExpression();
+ Expression selectable = selectableOfEnclosingSwitch();
+ if (selectable == null) {
+ super.visitCaseStatement(statement);
+ return;
+ }
Expression expression = statement.getExpression();
if (expression instanceof ClassExpression) { // GROOVY-8411: refine the switch type
if (!optInstanceOfTypeInfo(selectable, expression.getType())) {
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java
index cfb710179b..94e461f500 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingContext.java
@@ -27,6 +27,7 @@ import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.expr.SwitchExpression;
import org.codehaus.groovy.ast.stmt.SwitchStatement;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
@@ -229,6 +230,17 @@ public class TypeCheckingContext {
protected final LinkedList<Expression> enclosingMethodCalls = new LinkedList<>();
/** Stack of enclosing switch statements. */
protected final LinkedList<SwitchStatement> switchStatements = new LinkedList<>();
+ /** Stack of enclosing switch expressions (GROOVY-12255). */
+ protected final LinkedList<SwitchExpression> switchExpressions = new LinkedList<>();
+ /** Yield types collected for the current switch expression (parallel to {@link #switchExpressions}). */
+ protected final LinkedList<List<ClassNode>> switchExpressionYieldTypes = new LinkedList<>();
+ /**
+ * Innermost-first stack of switch selectors (statement or expression).
+ * Case-label typing must use this rather than preferring statements over
+ * expressions, or a nested switch expression would see the outer selector
+ * (GROOVY-12255).
+ */
+ protected final LinkedList<Expression> switchSelectors = new LinkedList<>();
/** Stack of enclosing closures. */
protected final LinkedList<EnclosingClosure> enclosingClosures = new LinkedList<>();
// stores the current binary expression. This is used when assignments are made with a null object, for type inference
@@ -410,12 +422,14 @@ public class TypeCheckingContext {
*/
public void pushEnclosingSwitchStatement(final SwitchStatement switchStatement) {
switchStatements.addFirst(switchStatement);
+ switchSelectors.addFirst(switchStatement.getExpression());
}
/**
* Pops a switch statement from the enclosing switch statements stack.
*/
public SwitchStatement popEnclosingSwitchStatement() {
+ switchSelectors.removeFirst();
return switchStatements.removeFirst();
}
@@ -436,6 +450,62 @@ public class TypeCheckingContext {
return Collections.unmodifiableList(switchStatements);
}
+ /**
+ * Pushes a switch expression onto the enclosing-expression stack
+ * (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public void pushEnclosingSwitchExpression(final SwitchExpression switchExpression) {
+ switchExpressions.addFirst(switchExpression);
+ switchExpressionYieldTypes.addFirst(new LinkedList<>());
+ switchSelectors.addFirst(switchExpression.getExpression());
+ }
+
+ /**
+ * Pops the current switch expression (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public SwitchExpression popEnclosingSwitchExpression() {
+ switchSelectors.removeFirst();
+ switchExpressionYieldTypes.removeFirst();
+ return switchExpressions.removeFirst();
+ }
+
+ /**
+ * Returns the switch expression on top of the stack, or {@code null}
+ * (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public SwitchExpression getEnclosingSwitchExpression() {
+ if (switchExpressions.isEmpty()) return null;
+ return switchExpressions.getFirst();
+ }
+
+ /**
+ * Returns the list collecting yield types for the current switch expression
+ * (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public List<ClassNode> getEnclosingSwitchExpressionYieldTypes() {
+ if (switchExpressionYieldTypes.isEmpty()) return List.of();
+ return switchExpressionYieldTypes.getFirst();
+ }
+
+ /**
+ * Selector of the innermost enclosing switch statement or expression
+ * (GROOVY-12255).
+ *
+ * @since 6.0.0
+ */
+ public Expression getEnclosingSwitchSelector() {
+ if (switchSelectors.isEmpty()) return null;
+ return switchSelectors.getFirst();
+ }
+
/**
* Represents the context of an enclosing closure. An enclosing closure wraps
* a closure expression and the list of return types found in the closure body.
diff --git a/src/spec/doc/core-semantics.adoc b/src/spec/doc/core-semantics.adoc
index 3efc02e161..cbd1467fda 100644
--- a/src/spec/doc/core-semantics.adoc
+++ b/src/spec/doc/core-semantics.adoc
@@ -205,13 +205,38 @@ Switch supports the following kinds of comparisons:
NOTE: When using a closure case value, the default `it` parameter is actually the switch value (in our example, variable `x`).
-Groovy also supports switch expressions as shown in the following example:
+Groovy also supports switch expressions as specified by
+https://openjdk.org/jeps/361[JEP 361: Switch Expressions]
+(link:https://issues.apache.org/jira/browse/GROOVY-12255[GROOVY-12255]).
+A switch expression produces a value. Arms may use arrow labels
+(`case L ->`), which do not fall through, or traditional colon labels
+(`case L:`) with an explicit `yield`. A switch expression must complete by
+yielding a value or by throwing; unlike a switch statement, an unmatched
+selector throws `IllegalStateException`. Under `@TypeChecked` /
+`@CompileStatic` a non-exhaustive switch expression (no `default`, and not a
+complete enum) is a compile-time error. From Groovy 6 the compiler keeps the
+expression as first-class AST (`SwitchExpression` / `YieldStatement`) rather
+than rewriting it to a closure around a switch statement.
[source,groovy]
----
include::../test/SemanticsTest.groovy[tags=switch_expression,indent=0]
----
+Arrow arms that need more than one statement use a block and `yield`:
+
+[source,groovy]
+----
+include::../test/SemanticsTest.groovy[tags=switch_expression_yield,indent=0]
+----
+
+Multiple constants may share an arm, and Groovy's usual `isCase` matching
+(Class, regex, Collection, Closure) is available in both the statement and
+the expression forms. Under static compilation, when every case label is an
+`int`, `String` or enum constant, the compiler emits `tableswitch` /
+`lookupswitch` bytecode in the same style as `javac`
+(link:https://issues.apache.org/jira/browse/GROOVY-12255[GROOVY-12255]).
+
==== Looping structures
===== Classic for loop
@@ -556,6 +581,7 @@ Groovy supports many of the same kinds of expressions as Java, including:
----
assert 'bar' == switch('foo') {
case 'foo' -> 'bar'
+ default -> 'other'
}
----
| switch expressions
diff --git a/src/spec/test/SemanticsTest.groovy b/src/spec/test/SemanticsTest.groovy
index ff2e2b5355..285582bcde 100644
--- a/src/spec/test/SemanticsTest.groovy
+++ b/src/spec/test/SemanticsTest.groovy
@@ -184,11 +184,28 @@ final class SemanticsTest {
case 'Adam' -> 'Eve'
case 'Antony' -> 'Cleopatra'
case 'Bonnie' -> 'Clyde'
+ default -> 'Unknown'
}
// end::switch_expression[]
assert partner == 'Juliet'
}
+ @Test
+ void testSwitchExpressionYieldBlock() {
+ // tag::switch_expression_yield[]
+ int n = 2
+ int value = switch (n) {
+ case 1 -> 10
+ case 2 -> {
+ int doubled = n * 10
+ yield doubled
+ }
+ default -> throw new IllegalArgumentException(String.valueOf(n))
+ }
+ // end::switch_expression_yield[]
+ assert value == 20
+ }
+
@Test
void testClassicForLoop() {
// tag::classic_for_loop_example[]
diff --git a/src/test-resources/core/SwitchExpression_02x.groovy b/src/test-resources/core/SwitchExpression_02x.groovy
index 5574d94b67..f5cf4de9a3 100644
--- a/src/test-resources/core/SwitchExpression_02x.groovy
+++ b/src/test-resources/core/SwitchExpression_02x.groovy
@@ -24,7 +24,11 @@ def result = switch(a) {
assert 'a' == result
a = 9
-result = switch(a) {
- case 6 -> 'a'
+try {
+ result = switch(a) {
+ case 6 -> 'a'
+ }
+ assert false: 'expected IllegalStateException for non-exhaustive switch expression'
+} catch (IllegalStateException e) {
+ assert e.message.contains('does not cover')
}
-assert null == result
diff --git a/src/test-resources/core/SwitchExpression_08x.groovy b/src/test-resources/core/SwitchExpression_08x.groovy
index 0b171f2587..9d714c2136 100644
--- a/src/test-resources/core/SwitchExpression_08x.groovy
+++ b/src/test-resources/core/SwitchExpression_08x.groovy
@@ -36,7 +36,11 @@ result = switch(a) {
assert 'a' == result
a = 9
-result = switch(a) {
- case 6, 8 -> { yield 'a' }
+try {
+ result = switch(a) {
+ case 6, 8 -> { yield 'a' }
+ }
+ assert false: 'expected IllegalStateException for non-exhaustive switch expression'
+} catch (IllegalStateException e) {
+ assert e.message.contains('does not cover')
}
-assert null == result
diff --git a/src/test-resources/core/SwitchExpression_20x.groovy b/src/test-resources/core/SwitchExpression_20x.groovy
index f0eec8d7b4..16fb69f6a2 100644
--- a/src/test-resources/core/SwitchExpression_20x.groovy
+++ b/src/test-resources/core/SwitchExpression_20x.groovy
@@ -28,11 +28,12 @@ def meth() {
def r = switch (a) {
case 1 -> {
if (true) {
- same 'a'
+ yield same('a')
} else {
- same 'b'
+ yield same('b')
}
}
+ default -> same('z')
}
r + 'z'
}
diff --git a/src/test-resources/core/SwitchExpression_21x.groovy b/src/test-resources/core/SwitchExpression_21x.groovy
index 55bac5af7f..8bffb921f2 100644
--- a/src/test-resources/core/SwitchExpression_21x.groovy
+++ b/src/test-resources/core/SwitchExpression_21x.groovy
@@ -33,6 +33,7 @@ def meth() {
yield 'hi'
}
case 2 -> 'Hello'
+ default -> 'hi'
}
r + ', Daniel'
}
diff --git a/src/test-resources/core/SwitchExpression_02x.groovy b/src/test-resources/fail/SwitchExpression_11x.groovy
similarity index 85%
copy from src/test-resources/core/SwitchExpression_02x.groovy
copy to src/test-resources/fail/SwitchExpression_11x.groovy
index 5574d94b67..156234ba83 100644
--- a/src/test-resources/core/SwitchExpression_02x.groovy
+++ b/src/test-resources/fail/SwitchExpression_11x.groovy
@@ -17,14 +17,12 @@
* under the License.
*/
-def a = 6
-def result = switch(a) {
- case 6 -> 'a'
+def a = 1
+def r = switch (a) {
+ case 1 -> {
+ if (true) {
+ println 'no yield'
+ }
+ }
+ default -> 0
}
-assert 'a' == result
-
-a = 9
-result = switch(a) {
- case 6 -> 'a'
-}
-assert null == result
diff --git a/src/test-resources/core/SwitchExpression_02x.groovy b/src/test-resources/fail/SwitchExpression_12x.groovy
similarity index 85%
copy from src/test-resources/core/SwitchExpression_02x.groovy
copy to src/test-resources/fail/SwitchExpression_12x.groovy
index 5574d94b67..52e9369244 100644
--- a/src/test-resources/core/SwitchExpression_02x.groovy
+++ b/src/test-resources/fail/SwitchExpression_12x.groovy
@@ -17,14 +17,12 @@
* under the License.
*/
-def a = 6
-def result = switch(a) {
- case 6 -> 'a'
+def a = 1
+def r = switch (a) {
+ case 1 -> {
+ if (true) {
+ yield 1
+ }
+ }
+ default -> 0
}
-assert 'a' == result
-
-a = 9
-result = switch(a) {
- case 6 -> 'a'
-}
-assert null == result
diff --git a/src/test-resources/core/SwitchExpression_02x.groovy b/src/test-resources/fail/SwitchExpression_13x.groovy
similarity index 85%
copy from src/test-resources/core/SwitchExpression_02x.groovy
copy to src/test-resources/fail/SwitchExpression_13x.groovy
index 5574d94b67..deb81ae64a 100644
--- a/src/test-resources/core/SwitchExpression_02x.groovy
+++ b/src/test-resources/fail/SwitchExpression_13x.groovy
@@ -17,14 +17,12 @@
* under the License.
*/
-def a = 6
-def result = switch(a) {
- case 6 -> 'a'
+def a = 1
+def r = switch (a) {
+ case 1 -> {
+ for (;;) {
+ return 1
+ }
+ }
+ default -> 0
}
-assert 'a' == result
-
-a = 9
-result = switch(a) {
- case 6 -> 'a'
-}
-assert null == result
diff --git a/src/test/groovy/groovy/transform/stc/SwitchExpressionSTCTest.groovy b/src/test/groovy/groovy/transform/stc/SwitchExpressionSTCTest.groovy
new file mode 100644
index 0000000000..0fcee359b3
--- /dev/null
+++ b/src/test/groovy/groovy/transform/stc/SwitchExpressionSTCTest.groovy
@@ -0,0 +1,129 @@
+/*
+ * 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 groovy.transform.stc
+
+import org.junit.jupiter.api.Test
+
+/**
+ * Static type checking for first-class switch expressions (GROOVY-12255 / JEP 361).
+ */
+class SwitchExpressionSTCTest extends StaticTypeCheckingTestCase {
+
+ @Test
+ void testInferredTypeFromArms() {
+ assertScript '''
+ String s = switch (1) {
+ case 1 -> 'a'
+ default -> 'b'
+ }
+ assert s == 'a'
+ '''
+ }
+
+ @Test
+ void testIntResult() {
+ assertScript '''
+ int n = switch (2) {
+ case 1 -> 10
+ case 2 -> 20
+ default -> 0
+ }
+ assert n == 20
+ '''
+ }
+
+ @Test
+ void testExhaustiveEnumNeedsNoDefault() {
+ assertScript '''
+ import java.time.Month
+ import static java.time.Month.*
+
+ String q(Month m) {
+ switch (m) {
+ case JANUARY, FEBRUARY, MARCH -> 'Q1'
+ case APRIL, MAY, JUNE -> 'Q2'
+ case JULY, AUGUST, SEPTEMBER -> 'Q3'
+ case OCTOBER, NOVEMBER, DECEMBER -> 'Q4'
+ }
+ }
+ assert q(JUNE) == 'Q2'
+ '''
+ }
+
+ @Test
+ void testNonExhaustiveIsError() {
+ shouldFailWithMessages '''
+ def r = switch (1) {
+ case 1 -> 'a'
+ }
+ ''', 'the switch expression does not cover all possible input values'
+ }
+
+ @Test
+ void testYieldTypesAreUnified() {
+ assertScript '''
+ CharSequence cs = switch (1) {
+ case 1 -> 'hello'
+ default -> new StringBuilder('x')
+ }
+ assert cs.toString() in ['hello', 'x']
+ '''
+ }
+
+ @Test
+ void testNestedExpressionInsideSwitchStatementDifferentEnums() {
+ assertScript '''
+ enum Color { RED, BLUE }
+ enum Size { S, L }
+
+ 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 testAssignmentToOuterLocal() {
+ assertScript '''
+ int acc = 0
+ int r = switch (1) {
+ case 1 -> {
+ acc = 4
+ yield acc + 1
+ }
+ default -> 0
+ }
+ assert r == 5
+ assert acc == 4
+ '''
+ }
+}
diff --git a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index 7f1a591f1d..9c125aa618 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -630,6 +630,9 @@ final class SyntaxErrorTest {
TestUtils.doRunAndShouldFail('fail/SwitchExpression_08x.groovy')
TestUtils.doRunAndShouldFail('fail/SwitchExpression_09x.groovy')
TestUtils.doRunAndShouldFail('fail/SwitchExpression_10x.groovy')
+ TestUtils.doRunAndShouldFail('fail/SwitchExpression_11x.groovy')
+ TestUtils.doRunAndShouldFail('fail/SwitchExpression_12x.groovy')
+ TestUtils.doRunAndShouldFail('fail/SwitchExpression_13x.groovy')
}
@Test
diff --git a/src/test/groovy/org/apache/groovy/parser/antlr4/util/AstDumper.groovy b/src/test/groovy/org/apache/groovy/parser/antlr4/util/AstDumper.groovy
index 5e9a161599..d2ac5e11a9 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/util/AstDumper.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/util/AstDumper.groovy
@@ -68,6 +68,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression
import org.codehaus.groovy.ast.expr.SpreadExpression
import org.codehaus.groovy.ast.expr.SpreadMapExpression
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.ast.expr.SwitchExpression
import org.codehaus.groovy.ast.expr.TernaryExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.expr.UnaryMinusExpression
@@ -91,6 +92,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement
import org.codehaus.groovy.ast.stmt.ThrowStatement
import org.codehaus.groovy.ast.stmt.TryCatchStatement
import org.codehaus.groovy.ast.stmt.WhileStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
import org.codehaus.groovy.classgen.BytecodeExpression
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.classgen.Verifier
@@ -592,6 +594,32 @@ class AstNodeToScriptVisitor implements CompilationUnit.IPrimaryClassNodeOperati
printLineBreak()
}
+ @Override
+ void visitSwitchExpression(SwitchExpression expression) {
+ print 'switch ('
+ expression?.expression?.visit this
+ print ') {'
+ printLineBreak()
+ indented {
+ expression?.caseStatements?.each {
+ visitCaseStatement it
+ }
+ if (expression?.defaultStatement !instanceof EmptyStatement) {
+ print 'default -> '
+ printLineBreak()
+ expression?.defaultStatement?.visit this
+ }
+ }
+ print '}'
+ }
+
+ @Override
+ void visitYieldStatement(YieldStatement statement) {
+ print 'yield '
+ statement?.expression?.visit this
+ printLineBreak()
+ }
+
@Override
void visitSwitch(SwitchStatement statement) {
printStatementLabels(statement)
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
new file mode 100644
index 0000000000..5df6c52675
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/classgen/Groovy12255.groovy
@@ -0,0 +1,434 @@
+/*
+ * 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.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 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 typeCheckedUnmatchedIsError() {
+ def err = shouldFail('''
+ @groovy.transform.TypeChecked
+ def meth(int a) {
+ switch (a) {
+ case 1 -> 'one'
+ }
+ }
+ ''')
+ assert err.message.contains('does not cover all possible input values')
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/asm/SwitchExpressionBytecodeTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/asm/SwitchExpressionBytecodeTest.groovy
new file mode 100644
index 0000000000..43e6700092
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/SwitchExpressionBytecodeTest.groovy
@@ -0,0 +1,87 @@
+/*
+ * 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
+
+import org.junit.jupiter.api.Test
+
+/**
+ * GROOVY-12255: bytecode shape of first-class switch expressions — no closure
+ * wrapper, and tableswitch / lookupswitch when the selector and case labels
+ * permit it.
+ */
+final class SwitchExpressionBytecodeTest extends AbstractBytecodeTestCase {
+
+ @Test
+ void noClosureAllocationForSwitchExpression() {
+ def bytecode = compile(method: 'run', '''\
+ def r = switch (1) {
+ case 1 -> 'a'
+ default -> 'z'
+ }
+ ''')
+ assert !bytecode.hasStrictSequence(['INVOKESPECIAL', 'org/codehaus/groovy/runtime/callsite'])
+ assert !bytecode.toString().contains('InnerClassNode')
+ assert !bytecode.toString().contains('$_run_closure')
+ }
+
+ @Test
+ void staticIntSwitchUsesTableSwitch() {
+ def bytecode = compile(method: 'm', '''\
+ @groovy.transform.CompileStatic
+ int m(int n) {
+ switch (n) {
+ case 1 -> 10
+ case 2 -> 20
+ case 3 -> 30
+ default -> 0
+ }
+ }
+ ''')
+ assert bytecode.hasSequence(['TABLESWITCH']) || bytecode.hasSequence(['LOOKUPSWITCH'])
+ assert !bytecode.toString().contains('isCase')
+ }
+
+ @Test
+ void staticStringSwitchUsesLookupSwitch() {
+ def bytecode = compile(method: 'm', '''\
+ @groovy.transform.CompileStatic
+ String m(String s) {
+ switch (s) {
+ case 'Foo' -> 'a'
+ case 'Bar' -> 'b'
+ default -> 'z'
+ }
+ }
+ ''')
+ assert bytecode.hasSequence(['LOOKUPSWITCH']) || bytecode.hasSequence(['TABLESWITCH'])
+ assert bytecode.hasSequence(['INVOKEVIRTUAL java/lang/String.equals'])
+ }
+
+ @Test
+ void dynamicSwitchUsesIsCase() {
+ def bytecode = compile(method: 'run', '''\
+ def x = 'abc'
+ def r = switch (x) {
+ case String -> 1
+ default -> 2
+ }
+ ''')
+ assert bytecode.toString().contains('isCase') || bytecode.hasSequence(['INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.isCase'])
+ }
+}
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
index 55bd058417..cda94edb1b 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/AstNodeToScriptAdapter.groovy
@@ -68,6 +68,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression
import org.codehaus.groovy.ast.expr.SpreadExpression
import org.codehaus.groovy.ast.expr.SpreadMapExpression
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.ast.expr.SwitchExpression
import org.codehaus.groovy.ast.expr.TernaryExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.expr.UnaryMinusExpression
@@ -91,6 +92,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement
import org.codehaus.groovy.ast.stmt.ThrowStatement
import org.codehaus.groovy.ast.stmt.TryCatchStatement
import org.codehaus.groovy.ast.stmt.WhileStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
import org.codehaus.groovy.classgen.BytecodeExpression
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.classgen.Verifier
@@ -693,6 +695,34 @@ class AstNodeToScriptVisitor implements CompilationUnit.IPrimaryClassNodeOperati
printLineBreak()
}
+ /** Renders a first-class switch expression (GROOVY-12255). */
+ @Override
+ void visitSwitchExpression(SwitchExpression expression) {
+ print 'switch ('
+ expression?.expression?.visit this
+ print ') {'
+ printLineBreak()
+ indented {
+ expression?.caseStatements?.each {
+ visitCaseStatement it
+ }
+ if (expression?.defaultStatement !instanceof EmptyStatement) {
+ print 'default -> '
+ printLineBreak()
+ expression?.defaultStatement?.visit this
+ }
+ }
+ print '}'
+ }
+
+ /** Renders a yield statement (GROOVY-12255). */
+ @Override
+ void visitYieldStatement(YieldStatement statement) {
+ print 'yield '
+ statement?.expression?.visit this
+ printLineBreak()
+ }
+
/** Renders a switch statement. */
@Override
void visitSwitch(SwitchStatement statement) {
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ScriptToTreeNodeAdapter.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ScriptToTreeNodeAdapter.groovy
index 1238556507..14933dabe6 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ScriptToTreeNodeAdapter.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/ScriptToTreeNodeAdapter.groovy
@@ -70,6 +70,7 @@ import org.codehaus.groovy.ast.expr.RangeExpression
import org.codehaus.groovy.ast.expr.SpreadExpression
import org.codehaus.groovy.ast.expr.SpreadMapExpression
import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.ast.expr.SwitchExpression
import org.codehaus.groovy.ast.expr.TernaryExpression
import org.codehaus.groovy.ast.expr.TupleExpression
import org.codehaus.groovy.ast.expr.UnaryMinusExpression
@@ -93,6 +94,7 @@ import org.codehaus.groovy.ast.stmt.SynchronizedStatement
import org.codehaus.groovy.ast.stmt.ThrowStatement
import org.codehaus.groovy.ast.stmt.TryCatchStatement
import org.codehaus.groovy.ast.stmt.WhileStatement
+import org.codehaus.groovy.ast.stmt.YieldStatement
import org.codehaus.groovy.classgen.BytecodeExpression
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.classgen.asm.BytecodeHelper
@@ -637,6 +639,18 @@ class TreeNodeBuildingVisitor extends CodeVisitorSupport {
addNode(node, SwitchStatement, { super.visitSwitch(it) })
}
+ /** Adds a first-class switch expression node to the tree (GROOVY-12255). */
+ @Override
+ void visitSwitchExpression(SwitchExpression node) {
+ addNode(node, SwitchExpression, { super.visitSwitchExpression(it) })
+ }
+
+ /** Adds a yield statement node to the tree (GROOVY-12255). */
+ @Override
+ void visitYieldStatement(YieldStatement node) {
+ addNode(node, YieldStatement, { super.visitYieldStatement(it) })
+ }
+
/** Adds a case statement node to the tree. */
@Override
void visitCaseStatement(CaseStatement node) {
diff --git a/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ASTFinder.java b/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ASTFinder.java
index 6b8d83ba53..9001b2f8e2 100644
--- a/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ASTFinder.java
+++ b/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ASTFinder.java
@@ -252,6 +252,20 @@ class ASTFinder extends ContextualClassCodeVisitor {
tryFind(SwitchStatement.class, statement);
}
+ /** Visits a first-class switch expression and records it when it matches the pattern (GROOVY-12255). */
+ @Override
+ public void visitSwitchExpression(final org.codehaus.groovy.ast.expr.SwitchExpression expression) {
+ super.visitSwitchExpression(expression);
+ tryFind(org.codehaus.groovy.ast.expr.SwitchExpression.class, expression);
+ }
+
+ /** Visits a yield statement and records it when it matches the pattern (GROOVY-12255). */
+ @Override
+ public void visitYieldStatement(final org.codehaus.groovy.ast.stmt.YieldStatement statement) {
+ super.visitYieldStatement(statement);
+ tryFind(org.codehaus.groovy.ast.stmt.YieldStatement.class, statement);
+ }
+
/** Visits a synchronized statement and records it when it matches the pattern. */
@Override
public void visitSynchronizedStatement(final SynchronizedStatement statement) {
diff --git a/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ContextualClassCodeVisitor.java b/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ContextualClassCodeVisitor.java
index 7d9601e656..9266a9a537 100644
--- a/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ContextualClassCodeVisitor.java
+++ b/subprojects/groovy-macro/src/main/groovy/org/codehaus/groovy/macro/matcher/ContextualClassCodeVisitor.java
@@ -606,6 +606,22 @@ public abstract class ContextualClassCodeVisitor extends ClassCodeVisitorSupport
popContext();
}
+ /** Visits a first-class switch expression while tracking traversal context (GROOVY-12255). */
+ @Override
+ public void visitSwitchExpression(final org.codehaus.groovy.ast.expr.SwitchExpression expression) {
+ pushContext(expression);
+ super.visitSwitchExpression(expression);
+ popContext();
+ }
+
+ /** Visits a yield statement while tracking traversal context (GROOVY-12255). */
+ @Override
+ public void visitYieldStatement(final org.codehaus.groovy.ast.stmt.YieldStatement statement) {
+ pushContext(statement);
+ super.visitYieldStatement(statement);
+ popContext();
+ }
+
/** Visits a synchronized statement while tracking traversal context. */
@Override
public void visitSynchronizedStatement(final SynchronizedStatement statement) {