Support Java 14 multi-statement switch expressions (#107) - #162
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #107.
Java 14 switch expressions can use
yieldto 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:
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:
No symbol solver is configured, so the return type cannot be inferred and
SPECIFY_MEis 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.int x = switch (...)xx = switch (...)xdirectly (no temp)return switch (...)return tempfoo(switch ...)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. AddedConversionContext.PendingStatements: statement visitors push statements that must precede the current one, andVisitStatementsdrains 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 thanvar, sincevarrequires 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,
yieldbecomesreturn, selected by a nullYieldTargeton the context.Incidental fixes
YieldStmt, which was previously unhandled entirely.ArgumentOutOfRangeExceptioninSwitchExpressionVisitor: a fallthrough label with no statements readstatements[0]after only guarding against counts greater than one.Testing
Built TDD — each case was confirmed failing before implementation.
return, and assignment forms, plus colon form, arrow form with block bodies, fallthrough labels,throwarms, and a C# keyword (base) as an identifier. These compile the generated C# and assert on its runtime output.SwitchExpressionLoweringTestscovering the nested-lambda fallback (shape,yield→return, warning, and that single-expression arms are left alone) and the fallthrough regression.allowWarningsflag toFullIntegrationTests, which otherwise throws on any warning.Full suite: 298 passing, 0 failing.
🤖 Generated with Claude Code