diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java index 3c017d88..150e99a7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java @@ -416,7 +416,14 @@ public final class PbsAst { Span span(); } + public enum WildcardSwitchKind { + DEFAULT, + UNDERSCORE, + RECOVERY + } + public record WildcardSwitchPattern( + WildcardSwitchKind kind, Span span) implements SwitchPattern { } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java index 2a44a2e8..01ad48f8 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java @@ -522,12 +522,16 @@ final class PbsExprParser { private PbsAst.SwitchPattern parseSwitchPattern() { if (cursor.match(PbsTokenKind.DEFAULT)) { - return new PbsAst.WildcardSwitchPattern(span(cursor.previous().start(), cursor.previous().end())); + return new PbsAst.WildcardSwitchPattern( + PbsAst.WildcardSwitchKind.DEFAULT, + span(cursor.previous().start(), cursor.previous().end())); } if (cursor.check(PbsTokenKind.IDENTIFIER) && "_".equals(cursor.peek().lexeme())) { final var wildcard = cursor.advance(); - return new PbsAst.WildcardSwitchPattern(span(wildcard.start(), wildcard.end())); + return new PbsAst.WildcardSwitchPattern( + PbsAst.WildcardSwitchKind.UNDERSCORE, + span(wildcard.start(), wildcard.end())); } if (cursor.check(PbsTokenKind.IDENTIFIER) @@ -545,7 +549,9 @@ final class PbsExprParser { final var token = cursor.peek(); report(token, ParseErrors.E_PARSE_INVALID_SWITCH_FORM, "Invalid switch pattern"); cursor.advance(); - return new PbsAst.WildcardSwitchPattern(span(token.start(), token.end())); + return new PbsAst.WildcardSwitchPattern( + PbsAst.WildcardSwitchKind.RECOVERY, + span(token.start(), token.end())); } private PbsAst.Expression parseLiteralPatternExpression() { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java index 6f1f7aee..d0a837f4 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowExpressionAnalyzer.java @@ -850,17 +850,37 @@ final class PbsFlowExpressionAnalyzer { final var seenPatterns = new HashSet(); boolean hasWildcard = false; + PbsAst.WildcardSwitchKind wildcardKind = null; + int wildcardCount = 0; TypeView armType = null; for (final var arm : switchExpr.arms()) { - final var patternKey = switchPatternKey(arm.pattern()); - if (arm.pattern() instanceof PbsAst.WildcardSwitchPattern) { + if (arm.pattern() instanceof PbsAst.WildcardSwitchPattern wildcardPattern) { hasWildcard = true; - } else if (!seenPatterns.add(patternKey)) { - p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, - PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name(), - "Duplicate switch pattern", - arm.pattern().span()); + if (wildcardPattern.kind() != PbsAst.WildcardSwitchKind.RECOVERY) { + wildcardCount++; + if (wildcardCount == 1) { + wildcardKind = wildcardPattern.kind(); + } else if (wildcardKind != wildcardPattern.kind()) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name(), + "Switch wildcard arms cannot mix 'default' and '_'", + arm.pattern().span()); + } else { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name(), + "Duplicate switch wildcard pattern", + arm.pattern().span()); + } + } + } else { + final var patternKey = switchPatternKey(arm.pattern()); + if (!seenPatterns.add(patternKey)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name(), + "Duplicate switch pattern", + arm.pattern().span()); + } } final var patternType = switchPatternType(arm.pattern(), model, diagnostics); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsExprParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsExprParserTest.java index 0d008f00..031e82fd 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsExprParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsExprParserTest.java @@ -48,7 +48,7 @@ class PbsExprParserTest { final var source = """ fn flow(cond: bool, state: int) -> int { let a: int = if cond { 1 } else { 2 }; - let b: int = switch state { default: { 0 } }; + let b: int = switch state { default: { 0 }, _: { 1 } }; let c: int = handle run apply () { Err.fail -> Other.fail, _ -> { 1 } }; return 0; } @@ -69,7 +69,15 @@ class PbsExprParserTest { final var switchExpr = assertInstanceOf(PbsAst.SwitchExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(1)).initializer()); + assertEquals(2, switchExpr.arms().size()); + final var defaultPattern = assertInstanceOf(PbsAst.WildcardSwitchPattern.class, + switchExpr.arms().getFirst().pattern()); + assertEquals(PbsAst.WildcardSwitchKind.DEFAULT, defaultPattern.kind()); assertInstanceOf(PbsAst.IntLiteralExpr.class, switchExpr.arms().getFirst().block().tailExpression()); + final var underscorePattern = assertInstanceOf(PbsAst.WildcardSwitchPattern.class, + switchExpr.arms().get(1).pattern()); + assertEquals(PbsAst.WildcardSwitchKind.UNDERSCORE, underscorePattern.kind()); + assertInstanceOf(PbsAst.IntLiteralExpr.class, switchExpr.arms().get(1).block().tailExpression()); final var handle = assertInstanceOf(PbsAst.HandleExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer()); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsControlFlowTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsControlFlowTest.java index 7b8ee5ae..6b71d9e4 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsControlFlowTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsControlFlowTest.java @@ -5,6 +5,7 @@ import p.studio.compiler.pbs.PbsFrontendCompiler; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class PbsSemanticsControlFlowTest { @@ -39,4 +40,46 @@ class PbsSemanticsControlFlowTest { assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name()))); } + + @Test + void shouldRejectDuplicateSwitchWildcardArms() { + final var source = """ + fn flow(state: int) -> int { + let value: int = switch state { + default: { 1 }, + default: { 2 }, + }; + return value; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var duplicateWildcardCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name())) + .count(); + assertEquals(1, duplicateWildcardCount); + } + + @Test + void shouldRejectMixedSwitchWildcardSpellings() { + final var source = """ + fn flow(state: int) -> int { + let value: int = switch state { + default: { 1 }, + _: { 2 }, + }; + return value; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var duplicateWildcardCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name())) + .count(); + assertEquals(1, duplicateWildcardCount); + } }