From 5a0fd534bfd54591189be2803d7505a8f536d992 Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Sun, 16 Aug 2026 15:02:52 -0600 Subject: [PATCH 1/2] Support Java 21 record patterns (#67) Converts Java 21 record patterns (JEP 440) into C# patterns: - `instanceof` with a type or record pattern becomes a C# `is` pattern, so `o instanceof Point(int x, int y)` converts to a positional pattern. - Pattern labels in switch expressions and switch statements convert to C# patterns, including nested deconstruction and `var` components. - Java 21 `when` guards convert to C# `when` clauses. The parser language level is raised from Java 17 to Java 21, without which record patterns fail to parse at all. Switch statement break handling is also corrected. Java's arrow form never falls through, but previously only the default case received an implicit `break`, so an arrow-form pattern case would emit C# that fails to compile with CS0163. Breaks are now added for every arrow-form entry and suppressed when the section already ends in a jump statement, which additionally removes some unreachable breaks the old code emitted after a `return`. Co-Authored-By: Claude Opus 5 (1M context) --- .../ConvertRecordPatternTests.cs | 234 ++++++++++++++++++ JavaToCSharp.Tests/IntegrationTests.cs | 1 + .../Resources/Java21RecordPatterns.java | 55 ++++ .../InstanceOfExpressionVisitor.cs | 8 + .../Expressions/PatternExpressionVisitor.cs | 68 +++++ .../Expressions/SwitchExpressionVisitor.cs | 21 +- JavaToCSharp/JavaToCSharpConverter.cs | 2 +- .../Statements/SwitchStatementVisitor.cs | 86 ++++++- 8 files changed, 460 insertions(+), 15 deletions(-) create mode 100644 JavaToCSharp.Tests/ConvertRecordPatternTests.cs create mode 100644 JavaToCSharp.Tests/Resources/Java21RecordPatterns.java create mode 100644 JavaToCSharp/Expressions/PatternExpressionVisitor.cs diff --git a/JavaToCSharp.Tests/ConvertRecordPatternTests.cs b/JavaToCSharp.Tests/ConvertRecordPatternTests.cs new file mode 100644 index 00000000..aca680f8 --- /dev/null +++ b/JavaToCSharp.Tests/ConvertRecordPatternTests.cs @@ -0,0 +1,234 @@ +namespace JavaToCSharp.Tests; + +/// +/// Tests for Java 21 record patterns (JEP 440), which convert to C# positional patterns. +/// +public class ConvertRecordPatternTests +{ + [Fact] + public void InstanceOf_Record_Pattern_Is_Converted_To_Positional_Pattern() + { + const string javaCode = """ + package com.example; + public class Shapes { + record Point(int x, int y) {} + public boolean test(Object obj) { + return obj instanceof Point(int x, int y) && x > y; + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("obj is Point (int x, int y)", parsed); + } + + [Fact] + public void InstanceOf_Type_Pattern_Binds_The_Variable() + { + const string javaCode = """ + package com.example; + public class Shapes { + public String test(Object obj) { + if (obj instanceof String s) { + return s; + } + return ""; + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("obj is string s", parsed); + } + + [Fact] + public void InstanceOf_Without_Pattern_Remains_A_Plain_Type_Test() + { + const string javaCode = """ + package com.example; + public class Shapes { + public boolean test(Object obj) { + return obj instanceof String; + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("obj is string", parsed); + Assert.DoesNotContain("is string s", parsed); + } + + [Fact] + public void Nested_Record_Pattern_Is_Converted() + { + const string javaCode = """ + package com.example; + public class Shapes { + record Point(int x, int y) {} + record Line(Point start, Point end) {} + public boolean test(Object obj) { + return obj instanceof Line(Point(var ax, var ay), Point end); + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("obj is Line (Point (var ax, var ay), Point end)", parsed); + } + + [Fact] + public void Switch_Expression_Record_Pattern_Is_Converted() + { + const string javaCode = """ + package com.example; + public class Shapes { + record Point(int x, int y) {} + public String test(Object obj) { + return switch (obj) { + case Point(int x, int y) -> "point"; + default -> "other"; + }; + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("Point (int x, int y) =>", parsed); + } + + [Fact] + public void Switch_Expression_Guard_Is_Converted_To_When_Clause() + { + const string javaCode = """ + package com.example; + public class Shapes { + record Point(int x, int y) {} + public String test(Object obj) { + return switch (obj) { + case Point(int x, int y) when x > y -> "wide"; + default -> "other"; + }; + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("when x > y =>", parsed); + } + + [Fact] + public void Switch_Statement_Record_Pattern_Is_Converted_With_Guard() + { + const string javaCode = """ + package com.example; + public class Shapes { + record Point(int x, int y) {} + public String test(Object obj) { + switch (obj) { + case Point(int x, int y) when x > y: + return "wide"; + default: + return "other"; + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("case Point (int x, int y)when x > y:", parsed); + } + + /// + /// Java's arrow form never falls through, but C# requires each switch section to end with a + /// jump statement, so the conversion has to supply the break that Java leaves implicit. + /// + [Fact] + public void Arrow_Switch_Statement_Cases_Get_An_Implicit_Break() + { + const string javaCode = """ + package com.example; + public class Shapes { + record Point(int x, int y) {} + public void test(Object obj) { + switch (obj) { + case Point(int x, int y) -> System.out.println("point"); + default -> System.out.println("other"); + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("case Point (int x, int y):", parsed); + // Both the pattern case and the default case need a break. + Assert.Equal(2, parsed.Split("break;").Length - 1); + } + + /// + /// A break after a return would be unreachable, which C# rejects as an error. + /// + [Fact] + public void Cases_Ending_In_A_Jump_Statement_Do_Not_Get_An_Extra_Break() + { + const string javaCode = """ + package com.example; + public class Shapes { + public String test(Object obj) { + switch (obj) { + case String s -> { return s; } + default -> { return "other"; } + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.DoesNotContain("break;", parsed); + } + + /// + /// Colon-form entries keep Java's explicit fallthrough semantics, so no break is invented. + /// + [Fact] + public void Colon_Switch_Statement_Fallthrough_Is_Preserved() + { + const string javaCode = """ + package com.example; + public class Shapes { + public String test(int i) { + switch (i) { + case 1: + case 2: + return "low"; + default: + return "high"; + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("case 1:", parsed); + Assert.Contains("case 2:", parsed); + Assert.DoesNotContain("break;", parsed); + } + + private static string Convert(string javaCode) + { + var options = new JavaConversionOptions { IncludeComments = false }; + options.WarningEncountered += (_, eventArgs) + => throw new InvalidOperationException($"Encountered a warning in conversion: {eventArgs.Message}"); + + return JavaToCSharpConverter.ConvertText(javaCode, options) ?? ""; + } +} diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index f80d70d7..170daed1 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -22,6 +22,7 @@ public class IntegrationTests(ITestOutputHelper testOutputHelper) [InlineData("Resources/Java11LambdaInference.java")] [InlineData("Resources/MultidimensionalArrays.java", true)] [InlineData("Resources/Java17SealedClasses.java", true)] + [InlineData("Resources/Java21RecordPatterns.java")] public void GeneralSuccessfulConversionTest(string filePath, bool allowWarnings = false) { var options = new JavaConversionOptions diff --git a/JavaToCSharp.Tests/Resources/Java21RecordPatterns.java b/JavaToCSharp.Tests/Resources/Java21RecordPatterns.java new file mode 100644 index 00000000..719b8288 --- /dev/null +++ b/JavaToCSharp.Tests/Resources/Java21RecordPatterns.java @@ -0,0 +1,55 @@ +package com.example; + +/** + * Exercises Java 21 record patterns (JEP 440) in both instanceof tests and switch constructs. + */ +public class Java21RecordPatterns { + record Point(int x, int y) {} + + record Line(Point start, Point end) {} + + public String describeInstanceOf(Object obj) { + if (obj instanceof Point(int x, int y)) { + return "point " + x + "," + y; + } + + if (obj instanceof Line(Point(var ax, var ay), Point end)) { + return "line from " + ax + "," + ay; + } + + if (obj instanceof String s) { + return s; + } + + return "unknown"; + } + + public String describeSwitchExpression(Object obj) { + return switch (obj) { + case Point(int x, int y) when x > y -> "wide point"; + case Point(int x, int y) -> "point " + x + "," + y; + case Line(Point start, Point end) -> "line"; + case String s -> s; + default -> "unknown"; + }; + } + + public void describeArrowSwitchStatement(Object obj) { + switch (obj) { + case Point(int x, int y) -> System.out.println("point"); + case Line(Point start, Point end) -> System.out.println("line"); + default -> System.out.println("unknown"); + } + } + + public String describeColonSwitchStatement(Object obj) { + switch (obj) { + case Point(int x, int y): + return "point"; + case String s: + return s; + default: + return "unknown"; + } + } +} diff --git a/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs b/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs index 044f8d00..94d4413a 100644 --- a/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/InstanceOfExpressionVisitor.cs @@ -16,6 +16,14 @@ public class InstanceOfExpressionVisitor : ExpressionVisitor return null; } + // Java 16+ allows `x instanceof Shape s` and Java 21 `x instanceof Point(int a, int b)`. + // Both become C# `is` patterns; without a pattern this is a plain type test. + if (expr.getPattern().FromOptional() is { } pattern + && PatternExpressionVisitor.ConvertPattern(context, pattern) is { } patternSyntax) + { + return SyntaxFactory.IsPatternExpression(exprSyntax, patternSyntax); + } + var type = TypeHelper.ConvertTypeOf(expr); return SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, exprSyntax, SyntaxFactory.IdentifierName(type)); diff --git a/JavaToCSharp/Expressions/PatternExpressionVisitor.cs b/JavaToCSharp/Expressions/PatternExpressionVisitor.cs new file mode 100644 index 00000000..a5f79678 --- /dev/null +++ b/JavaToCSharp/Expressions/PatternExpressionVisitor.cs @@ -0,0 +1,68 @@ +using com.github.javaparser.ast.expr; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace JavaToCSharp.Expressions; + +/// +/// Converts Java pattern expressions (JEP 440/441) into C# patterns. +/// +/// +/// Java's type patterns (Shape s) map onto C# declaration patterns, and Java's record +/// deconstruction patterns (Point(int x, int y)) map onto C# positional patterns. +/// The shapes line up closely enough that the conversion is structural, but see +/// for the one case where they diverge. +/// +public static class PatternExpressionVisitor +{ + /// + /// Converts a Java pattern into the equivalent C# pattern, or returns null if the + /// pattern kind is not recognized. + /// + public static PatternSyntax? ConvertPattern(ConversionContext context, PatternExpr pattern) => + pattern switch + { + RecordPatternExpr recordPattern => ConvertRecordPattern(context, recordPattern), + TypePatternExpr typePattern => ConvertTypePattern(typePattern), + _ => null + }; + + /// + /// Converts a type pattern such as String s into a C# declaration pattern. + /// + private static PatternSyntax ConvertTypePattern(TypePatternExpr pattern) => + DeclarationPattern( + IdentifierName(TypeHelper.ConvertTypeOf(pattern)), + SingleVariableDesignation(Identifier(pattern.getNameAsString()))); + + /// + /// Converts a record deconstruction pattern such as Point(int x, int y) into a C# + /// positional pattern. + /// + private static PatternSyntax ConvertRecordPattern(ConversionContext context, RecordPatternExpr pattern) + { + var subpatterns = new List(); + + foreach (var component in pattern.getPatternList().ToList() ?? []) + { + // A nested `var x` component parses as a type pattern whose type is `var`. C# spells the + // equivalent as a plain variable designation, since `var x` is not valid as a subpattern type. + if (component is TypePatternExpr { } typeComponent && TypeHelper.ConvertTypeOf(typeComponent) == "var") + { + subpatterns.Add(Subpattern(VarPattern(SingleVariableDesignation(Identifier(typeComponent.getNameAsString()))))); + continue; + } + + if (ConvertPattern(context, component) is not { } converted) + { + return RecursivePattern(); + } + + subpatterns.Add(Subpattern(converted)); + } + + return RecursivePattern() + .WithType(IdentifierName(TypeHelper.ConvertTypeOf(pattern))) + .WithPositionalPatternClause(PositionalPatternClause(SeparatedList(subpatterns))); + } +} diff --git a/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs b/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs index 1a117642..0dfb8d76 100644 --- a/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs +++ b/JavaToCSharp/Expressions/SwitchExpressionVisitor.cs @@ -28,10 +28,19 @@ private SwitchExpressionArmSyntax Visit(ConversionContext context, SwitchEntry e var pattern = GetArmPatternSyntax(context, entry); var expr = GetArmExpressionSyntax(context, entry); - return SwitchExpressionArm( + var arm = SwitchExpressionArm( pattern, expr ); + + // Java 21 `case Foo f when cond ->` maps directly onto a C# `when` clause. + if (entry.getGuard().FromOptional() is { } guard + && VisitExpression(context, guard) is { } guardSyntax) + { + arm = arm.WithWhenClause(WhenClause(guardSyntax)); + } + + return arm; } private static PatternSyntax GetArmPatternSyntax(ConversionContext context, SwitchEntry entry) @@ -47,6 +56,16 @@ private static PatternSyntax GetArmPatternSyntax(ConversionContext context, Swit foreach (var label in labels) { + // Java 21 pattern labels (`case Point(int x, int y)`) are already patterns; everything + // else is a constant label and needs wrapping. + if (label is PatternExpr patternLabel) + { + patterns.Add(PatternExpressionVisitor.ConvertPattern(context, patternLabel) + ?? throw new InvalidOperationException( + $"Unsupported switch pattern label `{patternLabel}`")); + continue; + } + if (VisitExpression(context, label) is not ExpressionSyntax labelExpr) { throw new InvalidOperationException("Switch expression label must contain an expression"); diff --git a/JavaToCSharp/JavaToCSharpConverter.cs b/JavaToCSharp/JavaToCSharpConverter.cs index c9832106..20e94093 100644 --- a/JavaToCSharp/JavaToCSharpConverter.cs +++ b/JavaToCSharp/JavaToCSharpConverter.cs @@ -28,7 +28,7 @@ public static class JavaToCSharpConverter context.ConversionStateChanged(ConversionState.ParsingJavaAst); var parser = new JavaParser(); - parser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17); + parser.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); var parsed = parser.parse(wrapper); diff --git a/JavaToCSharp/Statements/SwitchStatementVisitor.cs b/JavaToCSharp/Statements/SwitchStatementVisitor.cs index 21612f24..04a1d2ab 100644 --- a/JavaToCSharp/Statements/SwitchStatementVisitor.cs +++ b/JavaToCSharp/Statements/SwitchStatementVisitor.cs @@ -34,20 +34,20 @@ public class SwitchStatementVisitor : StatementVisitor var statements = cs.getStatements().ToList(); var syntaxes = VisitStatements(context, statements); + // Java's arrow form (`case X ->`) never falls through, but C# still requires each + // section to end with a jump statement, so an implicit break is added. Colon-form + // entries keep Java's explicit fallthrough and are left alone. + if (!cs.getType().Equals(SwitchEntry.Type.STATEMENT_GROUP)) + { + AddImplicitBreak(syntaxes); + } + if (labels is not { Count: > 0 }) { // default case - var hasBreakStmt = false; - foreach (var syntax in syntaxes) + if (cs.getType().Equals(SwitchEntry.Type.STATEMENT_GROUP)) { - if (syntax.Kind() == SyntaxKind.BreakStatement) - { - hasBreakStmt = true; - } - } - if (!hasBreakStmt) - { - syntaxes.Add(SyntaxFactory.BreakStatement()); + AddImplicitBreak(syntaxes); } var defaultSyntax = SyntaxFactory.SwitchSection( @@ -57,10 +57,14 @@ public class SwitchStatementVisitor : StatementVisitor } else { + // A guard applies to the entry as a whole, so it is attached to each of its labels. + var guardSyntax = cs.getGuard().FromOptional() is { } guard + ? ExpressionVisitor.VisitExpression(context, guard) + : null; + var labelSyntaxes = labels - .Select(i => ExpressionVisitor.VisitExpression(context, i)) - .OfType() - .Select(SwitchLabelSyntax (i) => SyntaxFactory.CaseSwitchLabel(i)); + .Select(SwitchLabelSyntax? (label) => ConvertLabel(context, label, guardSyntax)) + .OfType(); var caseSyntax = SyntaxFactory.SwitchSection( SyntaxFactory.List(labelSyntaxes.ToList()), @@ -71,4 +75,60 @@ public class SwitchStatementVisitor : StatementVisitor return SyntaxFactory.SwitchStatement(selectorSyntax, SyntaxFactory.List(caseSyntaxes)); } + + /// + /// Appends a break to a switch section unless it already ends with a statement that + /// transfers control, which would make the added break unreachable. + /// + private static void AddImplicitBreak(List syntaxes) + { + if (syntaxes.Count > 0 && TransfersControl(syntaxes[^1])) + { + return; + } + + syntaxes.Add(SyntaxFactory.BreakStatement()); + } + + /// + /// Determines whether a statement unconditionally transfers control out of a switch section. + /// + private static bool TransfersControl(StatementSyntax statement) => + statement switch + { + // The arrow form's braces become a block, so the jump is one level down. + BlockSyntax block => block.Statements.Count > 0 && TransfersControl(block.Statements[^1]), + _ => statement.Kind() is SyntaxKind.BreakStatement + or SyntaxKind.ReturnStatement + or SyntaxKind.ThrowStatement + or SyntaxKind.ContinueStatement + or SyntaxKind.GotoStatement + }; + + /// + /// Converts a single Java case label, which may be a Java 21 pattern, into a C# switch label. + /// + private static SwitchLabelSyntax? ConvertLabel( + ConversionContext context, + Expression label, + ExpressionSyntax? guardSyntax) + { + if (label is PatternExpr patternLabel) + { + if (PatternExpressionVisitor.ConvertPattern(context, patternLabel) is not { } patternSyntax) + { + return null; + } + + var caseLabel = SyntaxFactory.CasePatternSwitchLabel(patternSyntax, SyntaxFactory.Token(SyntaxKind.ColonToken)); + + return guardSyntax is null + ? caseLabel + : caseLabel.WithWhenClause(SyntaxFactory.WhenClause(guardSyntax)); + } + + return ExpressionVisitor.VisitExpression(context, label) is { } labelExpr + ? SyntaxFactory.CaseSwitchLabel(labelExpr) + : null; + } } From f51ce74c348e85bb953c63be65cab1d73811d5fb Mon Sep 17 00:00:00 2001 From: Paul Irwin Date: Sun, 16 Aug 2026 15:12:14 -0600 Subject: [PATCH 2/2] Assert on converted output in the record pattern integration test The record pattern resource was in GeneralSuccessfulConversionTest, which only asserts the result is non-null. That catches conversion throwing but says nothing about whether the emitted C# is correct. Move it to FullIntegrationTests, which compiles the generated C# with Roslyn, executes it, and asserts on the runtime output declared in the resource's `/// - output:` header. The resource is rewritten as a runnable program covering instanceof, switch expressions, both arrow and colon switch statements, guards, and nested deconstruction. The expected output is the verbatim output of the Java source run under a JDK, so the test pins the converted C# to Java's actual behavior rather than to a hand-written guess. Co-Authored-By: Claude Opus 5 (1M context) --- JavaToCSharp.Tests/IntegrationTests.cs | 2 +- .../Resources/Java21RecordPatterns.java | 83 ++++++++++++++----- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/JavaToCSharp.Tests/IntegrationTests.cs b/JavaToCSharp.Tests/IntegrationTests.cs index 170daed1..ce827238 100644 --- a/JavaToCSharp.Tests/IntegrationTests.cs +++ b/JavaToCSharp.Tests/IntegrationTests.cs @@ -22,7 +22,6 @@ public class IntegrationTests(ITestOutputHelper testOutputHelper) [InlineData("Resources/Java11LambdaInference.java")] [InlineData("Resources/MultidimensionalArrays.java", true)] [InlineData("Resources/Java17SealedClasses.java", true)] - [InlineData("Resources/Java21RecordPatterns.java")] public void GeneralSuccessfulConversionTest(string filePath, bool allowWarnings = false) { var options = new JavaConversionOptions @@ -74,6 +73,7 @@ public void GeneralUnsuccessfulConversionTest(string filePath) [InlineData("Resources/Java14SwitchExpressionsYieldAssign.java", true)] [InlineData("Resources/Java15TextBlocks.java")] [InlineData("Resources/Java16Records.java")] + [InlineData("Resources/Java21RecordPatterns.java")] [InlineData("Resources/NewArrayLiteralBug.java")] [InlineData("Resources/OctalLiteralBug.java")] [InlineData("Resources/DeprecatedAnnotation.java")] diff --git a/JavaToCSharp.Tests/Resources/Java21RecordPatterns.java b/JavaToCSharp.Tests/Resources/Java21RecordPatterns.java index 719b8288..2485468e 100644 --- a/JavaToCSharp.Tests/Resources/Java21RecordPatterns.java +++ b/JavaToCSharp.Tests/Resources/Java21RecordPatterns.java @@ -1,55 +1,98 @@ -package com.example; +/// Expect: +/// - output: "point 1,2\nline 0,0\nstring hi\nunknown\nwide point\npoint 1,2\nline\nstring hi\nunknown\narrow point\narrow line\narrow unknown\ncolon point\ncolon hi\ncolon unknown\nnested 4\n" +package example; -/** - * Exercises Java 21 record patterns (JEP 440) in both instanceof tests and switch constructs. - */ -public class Java21RecordPatterns { - record Point(int x, int y) {} +// https://openjdk.org/jeps/440 - record Line(Point start, Point end) {} +public class Program { + // Members are declared public because Java's package-private default maps to C# private, + // which is a pre-existing converter behavior unrelated to record patterns. + public record Point(int x, int y) { + } + + public record Line(Point start, Point end) { + } - public String describeInstanceOf(Object obj) { + // instanceof with type and record patterns, including a nested deconstruction. + public static String describeInstanceOf(Object obj) { if (obj instanceof Point(int x, int y)) { return "point " + x + "," + y; } if (obj instanceof Line(Point(var ax, var ay), Point end)) { - return "line from " + ax + "," + ay; + return "line " + ax + "," + ay; } if (obj instanceof String s) { - return s; + return "string " + s; } return "unknown"; } - public String describeSwitchExpression(Object obj) { + // Switch expression with pattern labels and a guard. + public static String describeSwitchExpression(Object obj) { return switch (obj) { case Point(int x, int y) when x > y -> "wide point"; case Point(int x, int y) -> "point " + x + "," + y; case Line(Point start, Point end) -> "line"; - case String s -> s; + case String s -> "string " + s; default -> "unknown"; }; } - public void describeArrowSwitchStatement(Object obj) { + // Arrow-form switch statement: each case has an implicit break in Java. + public static String describeArrowSwitch(Object obj) { + String result; + switch (obj) { - case Point(int x, int y) -> System.out.println("point"); - case Line(Point start, Point end) -> System.out.println("line"); - default -> System.out.println("unknown"); + case Point(int x, int y) -> result = "arrow point"; + case Line(Point start, Point end) -> result = "arrow line"; + default -> result = "arrow unknown"; } + + return result; } - public String describeColonSwitchStatement(Object obj) { + // Colon-form switch statement with pattern labels. + public static String describeColonSwitch(Object obj) { switch (obj) { case Point(int x, int y): - return "point"; + return "colon point"; case String s: - return s; + return "colon " + s; default: - return "unknown"; + return "colon unknown"; + } + } + + public static void main(String[] args) { + Point point = new Point(1, 2); + Line line = new Line(new Point(0, 0), new Point(4, 5)); + + System.out.println(describeInstanceOf(point)); + System.out.println(describeInstanceOf(line)); + System.out.println(describeInstanceOf("hi")); + System.out.println(describeInstanceOf(42)); + + // The guard selects the first arm only when x > y. + System.out.println(describeSwitchExpression(new Point(9, 1))); + System.out.println(describeSwitchExpression(point)); + System.out.println(describeSwitchExpression(line)); + System.out.println(describeSwitchExpression("hi")); + System.out.println(describeSwitchExpression(42)); + + System.out.println(describeArrowSwitch(point)); + System.out.println(describeArrowSwitch(line)); + System.out.println(describeArrowSwitch(42)); + + System.out.println(describeColonSwitch(point)); + System.out.println(describeColonSwitch("hi")); + System.out.println(describeColonSwitch(42)); + + // Bindings from a nested pattern are usable in the matched branch. + if (line instanceof Line(Point(var ax, var ay), Point(var bx, var by))) { + System.out.println("nested " + (bx - ax)); } } }