Support C# 15 labeled break and continue with goto fallback - #173
Merged
Conversation
Java's labeled `break`/`continue` were previously converted to a plain `break`/`continue` with a warning, which silently changed the semantics: the jump targeted the innermost loop rather than the labeled one. Adds a `UseLabeledBreakAndContinue` option, enabled by default, which emits the C# 15 labeled jump syntax (`break outer;`). When disabled, the jumps are lowered to an equivalent `goto` with generated target labels, which is valid in every C# version. Roslyn cannot represent a labeled jump: there is no label operand on BreakStatementSyntax, and the parser rejects `break outer;` even at LanguageVersion.Preview. The C# 15 form is therefore emitted as a `goto` placeholder and substituted into the final text after normalization. The goto fallback places the continue target at the end of the loop body and the break target after the loop, emitting only the targets actually used. Closes #169 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upgrades Microsoft.CodeAnalysis.CSharp from 5.6.0 to 5.9.0, which adds real
support for both C# 15 features this project emits.
Labeled break/continue now use the new BreakStatement(IdentifierNameSyntax)
and ContinueStatement(IdentifierNameSyntax) overloads, replacing the `goto`
placeholder that was substituted into the final text. The sealed class
`closed` modifier now uses SyntaxKind.ClosedKeyword instead of fabricating
the token with ParseToken("closed ").
Both APIs are still marked experimental, so RSEXPERIMENTAL006 is suppressed
at each use site.
The integration test harness now parses generated code with
LanguageVersion.Preview, so the C# 15 labeled jump output is compiled and
executed rather than only string-asserted. LabeledBreakContinue.java runs
through both the default C# 15 path and the goto fallback, asserting the
same output for each.
Roslyn 5.9.0 depends on an unpublished Microsoft.CodeAnalysis.Analyzers
prerelease, so the released 5.9.0 is referenced explicitly to avoid NU1603.
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.
Closes #169
Problem
Java's labeled
break/continuewere converted to a plainbreak/continuewith a warning. That silently changed the semantics — the jump targeted the innermost loop instead of the labeled one, so the generated code compiled but did the wrong thing.Approach
Adds a
UseLabeledBreakAndContinueoption (default on), mirroring the existingUseClosedForSealedClassesprecedent for C# 15 features.Enabled — emits C# 15 labeled jumps, a near 1:1 mapping since C# puts the label directly on the loop just as Java does:
Disabled — lowers to
gotowith generated target labels, valid in every C# version. The continue target goes at the end of the loop body, the break target after the loop, and only the targets actually used are emitted:Roslyn 5.6.0 → 5.9.0
Roslyn 5.6.0 could not represent a labeled jump at all — no label operand on
BreakStatementSyntax, and the parser rejectedbreak outer;even atLanguageVersion.Preview. 5.9.0 adds real support, so this PR upgrades and uses the typed API throughout:BreakStatement(IdentifierNameSyntax)/ContinueStatement(IdentifierNameSyntax)for labeled jumps.SyntaxKind.ClosedKeywordfor the sealed-classclosedmodifier, replacing the pre-existingParseToken("closed ")hack that fabricated the token because noClosedKeywordexisted.Both APIs are still marked experimental, so
RSEXPERIMENTAL006is suppressed at each use site — a deliberate, narrow signal that these are preview APIs rather than a blanket project-level suppression.One packaging wart: Roslyn 5.9.0 declares a dependency on an unpublished
Microsoft.CodeAnalysis.Analyzersprerelease, which tripsNU1603under this repo'sTreatWarningsAsErrors. Referencing the released5.9.0explicitly satisfies the range without suppressing the warning class.Testing
ConvertLabeledBreakContinueTestscovering both modes, target placement, only-emit-used-targets, nested labels, and unlabeled jumps being unaffected.LabeledBreakContinue.javaruns throughFullIntegrationTests, which compiles and executes the generated C# and asserts on its output. The harness now parses withLanguageVersion.Preview, so the C# 15 path is genuinely executed, not just string-asserted.FullIntegrationTestsWithGotoFallbackruns the same Java through the fallback and asserts the identical output, so both lowerings are proven behaviorally equivalent. (They compile to distinct assembly names, sinceAssembly.LoadFilecaches by path and would otherwise silently reuse the first variant.)javac/javaexecution of the Java source; the generated C# produces byte-identical output.breakon aswitchand on a plain block were checked manually in both modes.Also wired up
The option is exposed in the CLI (
--no-labeled-break-and-continue) and the GUI settings window.Note for reviewers
C# 15 labeled jumps require
<LangVersion>preview</LangVersion>and .NET 11 to compile. Consumers on an older toolchain should turn the option off to get thegotofallback. Worth deciding whether the default should stay on given that constraint — it matches the existingUseClosedForSealedClassesdefault, so this PR keeps it consistent.🤖 Generated with Claude Code