You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/08/13 17:32:10 UTC

(groovy) branch GROOVY-12255 created (now 11215d66c7)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a change to branch GROOVY-12255
in repository https://gitbox.apache.org/repos/asf/groovy.git


      at 11215d66c7 GROOVY-12255: Compile switch expressions as first-class AST (no closure desugar)

This branch includes the following new commits:

     new 11215d66c7 GROOVY-12255: Compile switch expressions as first-class AST (no closure desugar)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(groovy) 01/01: GROOVY-12255: Compile switch expressions as first-class AST (no closure desugar)

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch GROOVY-12255
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 11215d66c725efdc5ff7308d67763b0b0e65dade
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 8f026ab81b..eabd9c26fc 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -235,6 +235,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 6d2e2b07be..a2c3b6d095 100644
--- a/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java
+++ b/src/main/java/org/codehaus/groovy/control/customizers/SecureASTCustomizer.java
@@ -59,6 +59,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;
@@ -82,6 +83,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;
@@ -1679,6 +1681,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 8809ded82d..2fa51084bc 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;
@@ -4878,6 +4880,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) {
@@ -4899,7 +5020,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) {