Skip to content

Support Java 14 multi-statement switch expressions (#107) - #162

Merged
paulirwin merged 2 commits into
masterfrom
issue/107-switch-expression-yield
Aug 16, 2026
Merged

Support Java 14 multi-statement switch expressions (#107)#162
paulirwin merged 2 commits into
masterfrom
issue/107-switch-expression-yield

Conversation

@paulirwin

@paulirwin paulirwin commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Fixes #107.

Java 14 switch expressions can use yield to produce a value from a block of statements. C# switch expression arms permit only a single expression, so these previously threw at conversion time.

Approach

Two strategies, picked by where the switch expression sits.

Where the statements can be hoisted out, lower to a plain switch statement assigning to a target:

int numLetters;
switch (day)
{
    case MONDAY:
    case FRIDAY:
        Console.WriteLine(6);
        numLetters = 6;
        break;
    default:
        throw new InvalidOperationException(...);
}

This compiles as-is — no placeholder to fill in, no delegate allocation, no closure-capture pitfalls.

Where they cannot — nested inside a larger expression — fall back to the invoked lambda from the issue:

Foo(x switch
{
    1 => ((Func<SPECIFY_ME>)(() =>
    {
        int a = 1;
        return a;
    }))(),
    _ => 20
});

No symbol solver is configured, so the return type cannot be inferred and SPECIFY_ME is emitted for the user to replace. That does not compile as-is, but converting with one spot to fix beats failing the whole file. Both strategies emit a warning.

Java Result
int x = switch (...) declaration + switch statement assigning x
x = switch (...) switch statement assigning x directly (no temp)
return switch (...) typed temp + switch + return temp
nested, e.g. foo(switch ...) invoked lambda with Func<SPECIFY_ME>

Arms that are already a single expression are untouched — including a block whose only statement is a yield — so existing conversions are unaffected and no lambda is introduced needlessly.

Implementation notes

Lowering needs to emit more than one statement, which the StatementSyntax? Visit(...) contract cannot express. Added ConversionContext.PendingStatements: statement visitors push statements that must precede the current one, and VisitStatements drains them. That method is the single choke point for every statement-list context (blocks, method bodies, constructor bodies), so one drain point covers all of them.

The drain is scoped by a depth watermark. Without it, nested statement lists — switch arm blocks — consume statements belonging to an outer list, which put the temp declaration inside the first switch section instead of before the switch. The integration tests caught this because they compile and execute the output; a syntax-comparison test would not have.

For return, the temporary is typed from the enclosing method's declared return type rather than var, since var requires an initializer. This avoids the type-inference problem in the hoistable cases entirely — the declared type is always available and always correct.

Inside the lambda fallback, yield becomes return, selected by a null YieldTarget on the context.

Incidental fixes

  • Registered a visitor for YieldStmt, which was previously unhandled entirely.
  • Fixed an ArgumentOutOfRangeException in SwitchExpressionVisitor: a fallthrough label with no statements read statements[0] after only guarding against counts greater than one.

Testing

Built TDD — each case was confirmed failing before implementation.

  • Three executable integration test resources covering the declaration, return, and assignment forms, plus colon form, arrow form with block bodies, fallthrough labels, throw arms, and a C# keyword (base) as an identifier. These compile the generated C# and assert on its runtime output.
  • SwitchExpressionLoweringTests covering the nested-lambda fallback (shape, yieldreturn, warning, and that single-expression arms are left alone) and the fallthrough regression.
  • Added an allowWarnings flag to FullIntegrationTests, which otherwise throws on any warning.

Full suite: 298 passing, 0 failing.

🤖 Generated with Claude Code

paulirwin and others added 2 commits August 15, 2026 21:27
Java 14 switch expressions may use `yield` to produce a value from a
block of statements. C# switch expression arms permit only a single
expression, so these could not be converted and threw at conversion
time.

Rather than emitting IIFE-style lambdas with a `Func<SPECIFY_ME>` cast
that the user must fix up by hand, lower multi-statement switch
expressions into plain switch statements that assign to a target:

    int numLetters;
    switch (day)
    {
        case MONDAY:
        case FRIDAY:
            Console.WriteLine(6);
            numLetters = 6;
            break;
        default:
            throw new InvalidOperationException(...);
    }

This produces code that compiles as-is and reads like a human wrote it.
A warning is emitted whenever the shape changes.

Lowering requires emitting more than one statement, which the
`StatementSyntax? Visit(...)` contract cannot express, so add
`ConversionContext.PendingStatements`. Statement visitors push
statements that must precede the current one, and `VisitStatements`
drains them. The drain is scoped by a depth watermark so that nested
statement lists (switch arm blocks) do not consume statements belonging
to an outer list.

Supported positions are those where the switch expression is the whole
initializer, assigned value, or returned value. Elsewhere there is
nowhere to hoist the statements to, and conversion fails with a message
explaining the limitation. For `return`, the temporary is typed from the
enclosing method's declared return type, since `var` requires an
initializer.

Also register a visitor for `YieldStmt`, which was previously unhandled
entirely, and fix an index-out-of-range in `SwitchExpressionVisitor`
where a fallthrough label with no statements read `statements[0]` after
only guarding against counts greater than one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When a multi-statement switch expression is nested inside a larger
expression, there is nowhere to hoist the statements to, so the
statement-level lowering does not apply. These previously failed
conversion outright.

Emit an immediately-invoked lambda for those arms instead, as described
in the issue:

    Foo(x switch
    {
        1 => ((Func<SPECIFY_ME>)(() =>
        {
            int a = 1;
            return a;
        }))(),
        _ => 20
    });

The return type cannot be inferred because no symbol solver is
configured, so `SPECIFY_ME` is emitted for the user to replace and a
warning says so. This does not compile as-is, but converting with one
spot to fix is better than failing the whole file.

Java's `yield` becomes `return` inside the lambda, selected by a null
`YieldTarget`. Arms that are already a single expression are untouched,
including a block whose only statement is a yield, so the common cases
keep their existing output and no lambda is introduced needlessly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@paulirwin
paulirwin marked this pull request as ready for review August 16, 2026 03:42
@paulirwin
paulirwin merged commit 144b192 into master Aug 16, 2026
5 checks passed
@paulirwin
paulirwin deleted the issue/107-switch-expression-yield branch August 16, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Java 14 switch expression multi-statement support

1 participant