Skip to content

Support C# 15 labeled break and continue with goto fallback - #173

Merged
paulirwin merged 2 commits into
masterfrom
fix/labeled-break-continue
Aug 17, 2026
Merged

Support C# 15 labeled break and continue with goto fallback#173
paulirwin merged 2 commits into
masterfrom
fix/labeled-break-continue

Conversation

@paulirwin

@paulirwin paulirwin commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Closes #169

Problem

Java's labeled break/continue were converted to a plain break/continue with 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 UseLabeledBreakAndContinue option (default on), mirroring the existing UseClosedForSealedClasses precedent 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:

outer:
    for (int i = 0; i < 4; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            if (j == 1) continue outer;
            if (i == 3) break outer;
        }
    }

Disabled — lowers to goto with 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:

{
    for (int i = 0; i < 4; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            if (j == 1) goto outer_continue;
            if (i == 3) goto outer_break;
        }
        outer_continue: ;
    }
    outer_break: ;
}

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 rejected break outer; even at LanguageVersion.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.ClosedKeyword for the sealed-class closed modifier, replacing the pre-existing ParseToken("closed ") hack that fabricated the token because no ClosedKeyword existed.

Both APIs are still marked experimental, so RSEXPERIMENTAL006 is 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.Analyzers prerelease, which trips NU1603 under this repo's TreatWarningsAsErrors. Referencing the released 5.9.0 explicitly satisfies the range without suppressing the warning class.

Testing

  • New ConvertLabeledBreakContinueTests covering both modes, target placement, only-emit-used-targets, nested labels, and unlabeled jumps being unaffected.
  • LabeledBreakContinue.java runs through FullIntegrationTests, which compiles and executes the generated C# and asserts on its output. The harness now parses with LanguageVersion.Preview, so the C# 15 path is genuinely executed, not just string-asserted.
  • A companion FullIntegrationTestsWithGotoFallback runs the same Java through the fallback and asserts the identical output, so both lowerings are proven behaviorally equivalent. (They compile to distinct assembly names, since Assembly.LoadFile caches by path and would otherwise silently reuse the first variant.)
  • The expected output was verified against actual javac/java execution of the Java source; the generated C# produces byte-identical output.
  • Labeled break on a switch and on a plain block were checked manually in both modes.
  • Full suite: 358 passed, 0 failed.

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 the goto fallback. Worth deciding whether the default should stay on given that constraint — it matches the existing UseClosedForSealedClasses default, so this PR keeps it consistent.

🤖 Generated with Claude Code

paulirwin and others added 2 commits August 17, 2026 10:59
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>
@paulirwin
paulirwin marked this pull request as ready for review August 17, 2026 18:02
@paulirwin
paulirwin merged commit d3ab6db into master Aug 17, 2026
5 checks passed
@paulirwin
paulirwin deleted the fix/labeled-break-continue branch August 17, 2026 18:02
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.

Support C# 15 labeled break and continue with goto fallback

1 participant