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&nbsp;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) {