diff --git a/docs/pbs/pull-requests/INDEX.md b/docs/pbs/pull-requests/INDEX.md index ad614289..3f94e83b 100644 --- a/docs/pbs/pull-requests/INDEX.md +++ b/docs/pbs/pull-requests/INDEX.md @@ -8,10 +8,12 @@ Este indice organiza uma sequencia de PRs atomicas para levar `frontends/pbs` ao 4. `PR-004-pbs-parser-statements-and-control-flow.md` 5. `PR-005-pbs-parser-expressions-optional-result-apply.md` 6. `PR-006-pbs-barrel-and-module-visibility.md` -7. `PR-007-pbs-static-semantics-declaration-validation.md` -8. `PR-008-pbs-static-semantics-call-resolution-and-flow.md` -9. `PR-009-pbs-diagnostics-contract-v1.md` -10. `PR-010-pbs-irbackend-lowering-contract.md` -11. `PR-011-pbs-gate-u-conformance-fixtures.md` +7. `PR-006.2-pbs-parser-ast-syntax-hardening.md` +8. `PR-006.3-pbs-syntax-completeness-and-module-hygiene.md` +9. `PR-007-pbs-static-semantics-declaration-validation.md` +10. `PR-008-pbs-static-semantics-call-resolution-and-flow.md` +11. `PR-009-pbs-diagnostics-contract-v1.md` +12. `PR-010-pbs-irbackend-lowering-contract.md` +13. `PR-011-pbs-gate-u-conformance-fixtures.md` Cada documento e auto contido e inclui: `Briefing`, `Target`, `Method`, `Acceptance Criteria` e `Tests`. diff --git a/docs/pbs/pull-requests/PR-006.3-pbs-syntax-completeness-and-module-hygiene.md b/docs/pbs/pull-requests/PR-006.3-pbs-syntax-completeness-and-module-hygiene.md new file mode 100644 index 00000000..13482483 --- /dev/null +++ b/docs/pbs/pull-requests/PR-006.3-pbs-syntax-completeness-and-module-hygiene.md @@ -0,0 +1,42 @@ +# PR-006.3 - PBS Syntax Completeness and Module Hygiene + +## Briefing +Depois do hardening sintatico principal, ainda restam lacunas de completude do contrato de sintaxe/modulo que afetam determinismo diagnostico e aderencia fina ao spec. +Este PR fecha essas lacunas com foco em regras formais e higiene de modulo. + +## Target +- Specs: + - `docs/pbs/specs/3. Core Syntax Specification.md` (secoes 5.1, 6.1.1, 8, 12) + - `docs/pbs/specs/12. Diagnostics Specification.md` (phase = syntax/linking) +- Codigo: + - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java` + - `.../pbs/parser/PbsExprParser.java` + - `.../pbs/lexer/PbsLexer.java` + - `.../pbs/linking/PbsModuleVisibilityValidator.java` + +## Method +1. Atributos (`AttrList`) em `.pbs`: + - introduzir parse minimo de atributo no frontend; + - em modulo ordinario, rejeitar com diagnostico especifico e recuperacao estavel. +2. Regras de modulo: + - validar erro quando modulo possui `mod.barrel` mas zero arquivos `.pbs`. +3. Ajustes de forma sintatica: + - aceitar trailing comma em `StructFieldList`; + - aplicar limites de aridade: tupla tipo (1..6) e tupla literal (2..6). +4. Lexer/string: + - diagnosticar escape de string invalido de forma deterministica (sem aceitar silenciosamente). + +## Acceptance Criteria +- Uso de atributos em modulo ordinario gera erro deterministico com span primario no atributo. +- Modulo sem `.pbs` e com `mod.barrel` nao passa silenciosamente. +- `declare struct S(a: int,);` passa no parser. +- Tupla tipo com mais de 6 campos falha deterministicamente. +- Tupla literal com mais de 6 itens falha deterministicamente. +- Escape invalido em string gera erro lexico dedicado. + +## Tests +- Novo teste de parser para atributos em `.pbs` com recuperacao e codigo estavel. +- Novo teste de linking para modulo sem `.pbs`. +- Testes de parser para trailing comma em struct fields. +- Testes de parser para limites de aridade de tupla tipo/tupla literal. +- Teste de lexer para escape invalido em string. 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 9c15443d..3c017d88 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 @@ -84,7 +84,6 @@ public final class PbsAst { ReturnKind returnKind, TypeRef returnType, TypeRef resultErrorType, - Expression elseFallback, Block body, Span span) implements TopDecl { } @@ -92,6 +91,8 @@ public final class PbsAst { public record StructDecl( String name, ReadOnlyList fields, + ReadOnlyList methods, + ReadOnlyList ctors, boolean hasBody, Span span) implements TopDecl { } @@ -140,6 +141,7 @@ public final class PbsAst { String contractName, String ownerName, String binderName, + ReadOnlyList methods, Span span) implements TopDecl { } @@ -154,6 +156,13 @@ public final class PbsAst { Span span) { } + public record CtorDecl( + String name, + ReadOnlyList parameters, + Block body, + Span span) { + } + public record TypeRef( TypeRefKind kind, String name, @@ -241,6 +250,7 @@ public final class PbsAst { public record Block( ReadOnlyList statements, + Expression tailExpression, Span span) { } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java index f36a5e0b..3a681294 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java @@ -12,6 +12,9 @@ public enum ParseErrors { E_PARSE_INVALID_ASSIGN_TARGET, E_PARSE_INVALID_FOR_FORM, E_PARSE_LOOP_CONTROL_OUTSIDE_LOOP, + E_PARSE_INVALID_ENUM_FORM, + E_PARSE_DUPLICATE_ENUM_CASE_LABEL, + E_PARSE_DUPLICATE_ENUM_CASE_ID, E_PARSE_INVALID_SWITCH_FORM, E_PARSE_INVALID_HANDLE_FORM, E_PARSE_INVALID_TUPLE_LITERAL, 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 f08575a7..81879676 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 @@ -14,17 +14,25 @@ import java.util.ArrayList; * Dedicated expression parser for PBS. */ final class PbsExprParser { + @FunctionalInterface + interface BlockParserDelegate { + PbsAst.Block parse(String message); + } + private final PbsTokenCursor cursor; private final FileId fileId; private final DiagnosticSink diagnostics; + private final BlockParserDelegate blockParserDelegate; PbsExprParser( final PbsTokenCursor cursor, final FileId fileId, - final DiagnosticSink diagnostics) { + final DiagnosticSink diagnostics, + final BlockParserDelegate blockParserDelegate) { this.cursor = cursor; this.fileId = fileId; this.diagnostics = diagnostics; + this.blockParserDelegate = blockParserDelegate; } /** @@ -592,26 +600,7 @@ final class PbsExprParser { } private PbsAst.Block parseSurfaceBlock(final String message) { - final var open = consume(PbsTokenKind.LEFT_BRACE, message); - return parseSurfaceBlockFromOpen(open); - } - - private PbsAst.Block parseSurfaceBlockFromOpen(final PbsToken open) { - var depth = 1; - var end = open.end(); - while (!cursor.isAtEnd() && depth > 0) { - final var token = cursor.advance(); - end = token.end(); - if (token.kind() == PbsTokenKind.LEFT_BRACE) { - depth++; - } else if (token.kind() == PbsTokenKind.RIGHT_BRACE) { - depth--; - } - } - if (depth > 0) { - report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected '}' to close block expression"); - } - return new PbsAst.Block(ReadOnlyList.empty(), span(open.start(), end)); + return blockParserDelegate.parse(message); } private PbsToken consume(final PbsTokenKind kind, final String message) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java index e25e5314..ec2bb866 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java @@ -9,6 +9,7 @@ import p.studio.compiler.source.identifiers.FileId; import p.studio.utilities.structures.ReadOnlyList; import java.util.ArrayList; +import java.util.HashSet; /** * High-level manual parser for PBS source files. @@ -29,7 +30,7 @@ public final class PbsParser { final FileId fileId, final DiagnosticSink diagnostics) { this.cursor = new PbsTokenCursor(tokens); - this.exprParser = new PbsExprParser(cursor, fileId, diagnostics); + this.exprParser = new PbsExprParser(cursor, fileId, diagnostics, this::parseExpressionSurfaceBlock); this.fileId = fileId; this.diagnostics = diagnostics; } @@ -205,6 +206,8 @@ public final class PbsParser { final var fields = parseStructFields(); consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after struct fields"); + final var methods = new ArrayList(); + final var ctors = new ArrayList(); final boolean hasBody; long end; if (cursor.match(PbsTokenKind.SEMICOLON)) { @@ -212,7 +215,10 @@ public final class PbsParser { end = cursor.previous().end(); } else if (cursor.match(PbsTokenKind.LEFT_BRACE)) { hasBody = true; - end = parseStructBodyAndConsumeRightBrace(cursor.previous()); + final var bodyParse = parseStructBodyAndConsumeRightBrace(cursor.previous()); + methods.addAll(bodyParse.methods().asList()); + ctors.addAll(bodyParse.ctors().asList()); + end = bodyParse.end(); } else { report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, "Struct declaration must end with ';' or contain a '{...}' body"); @@ -220,7 +226,13 @@ public final class PbsParser { end = consumeDeclarationTerminator(); } - return new PbsAst.StructDecl(name.lexeme(), ReadOnlyList.wrap(fields), hasBody, span(declareToken.start(), end)); + return new PbsAst.StructDecl( + name.lexeme(), + ReadOnlyList.wrap(fields), + ReadOnlyList.wrap(methods), + ReadOnlyList.wrap(ctors), + hasBody, + span(declareToken.start(), end)); } private ArrayList parseStructFields() { @@ -258,14 +270,16 @@ public final class PbsParser { return fields; } - private long parseStructBodyAndConsumeRightBrace(final PbsToken leftBrace) { + private StructBodyParse parseStructBodyAndConsumeRightBrace(final PbsToken leftBrace) { + final var methods = new ArrayList(); + final var ctors = new ArrayList(); while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { if (cursor.match(PbsTokenKind.FN)) { - parseMethodDeclarationInBody(cursor.previous()); + methods.add(parseFunctionLike(cursor.previous())); continue; } if (cursor.match(PbsTokenKind.CTOR)) { - parseCtorDeclarationInBody(cursor.previous()); + ctors.add(parseCtorDeclarationInBody(cursor.previous())); continue; } report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, @@ -273,15 +287,20 @@ public final class PbsParser { cursor.advance(); } final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end struct body"); - return rightBrace.end(); + return new StructBodyParse(ReadOnlyList.wrap(methods), ReadOnlyList.wrap(ctors), rightBrace.end()); } - private void parseCtorDeclarationInBody(final PbsToken ctorToken) { - consume(PbsTokenKind.IDENTIFIER, "Expected constructor name"); + private PbsAst.CtorDecl parseCtorDeclarationInBody(final PbsToken ctorToken) { + final var name = consume(PbsTokenKind.IDENTIFIER, "Expected constructor name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after constructor name"); - parseParametersUntilRightParen(); + final var parameters = parseParametersUntilRightParen(); consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after constructor parameters"); - consumeBlockForMember("constructor"); + final var body = parseBlock(); + return new PbsAst.CtorDecl( + name.lexeme(), + ReadOnlyList.wrap(parameters), + body, + span(ctorToken.start(), body.span().getEnd())); } private PbsAst.ContractDecl parseContractDeclaration(final PbsToken declareToken) { @@ -312,7 +331,7 @@ public final class PbsParser { cursor.advance(); continue; } - methods.add(parseFunctionLike(cursor.previous(), false)); + methods.add(parseFunctionLike(cursor.previous())); } final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end service body"); return new PbsAst.ServiceDecl(name.lexeme(), ReadOnlyList.wrap(methods), span(declareToken.start(), rightBrace.end())); @@ -336,6 +355,10 @@ public final class PbsParser { consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after enum name"); final var cases = new ArrayList(); + final var caseLabels = new HashSet(); + final var explicitCaseIds = new HashSet(); + var hasExplicitCases = false; + var hasImplicitCases = false; if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { do { final var caseName = consume(PbsTokenKind.IDENTIFIER, "Expected enum case label"); @@ -343,11 +366,32 @@ public final class PbsParser { if (cursor.match(PbsTokenKind.EQUAL)) { final var intToken = consume(PbsTokenKind.INT_LITERAL, "Expected integer literal after '=' in enum case"); explicitValue = parseLongOrNull(intToken.lexeme()); + hasExplicitCases = true; + if (explicitValue == null) { + report(intToken, ParseErrors.E_PARSE_INVALID_ENUM_FORM, + "Invalid explicit enum identifier"); + } else if (!explicitCaseIds.add(explicitValue)) { + report(intToken, ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_ID, + "Duplicate explicit enum identifier '%s'".formatted(explicitValue)); + } + } else { + hasImplicitCases = true; } + + if (!caseLabels.add(caseName.lexeme())) { + report(caseName, ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_LABEL, + "Duplicate enum case label '%s'".formatted(caseName.lexeme())); + } + cases.add(new PbsAst.EnumCase(caseName.lexeme(), explicitValue, span(caseName.start(), cursor.previous().end()))); } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN)); } + if (hasExplicitCases && hasImplicitCases) { + report(name, ParseErrors.E_PARSE_INVALID_ENUM_FORM, + "Enum declarations cannot mix implicit and explicit case identifiers"); + } + final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after enum cases"); final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after enum declaration"); return new PbsAst.EnumDecl(name.lexeme(), ReadOnlyList.wrap(cases), span(declareToken.start(), Math.max(rightParen.end(), semicolon.end()))); @@ -357,10 +401,10 @@ public final class PbsParser { * Parses a top-level function declaration. */ private PbsAst.FunctionDecl parseFunction(final PbsToken fnToken) { - return parseFunctionLike(fnToken, true); + return parseFunctionLike(fnToken); } - private PbsAst.FunctionDecl parseFunctionLike(final PbsToken fnToken, final boolean allowElseFallback) { + private PbsAst.FunctionDecl parseFunctionLike(final PbsToken fnToken) { final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); final var parameters = parseParametersUntilRightParen(); @@ -368,11 +412,6 @@ public final class PbsParser { final var returnSpec = parseCallableReturnSpec(rightParen); - PbsAst.Expression elseFallback = null; - if (allowElseFallback && cursor.match(PbsTokenKind.ELSE)) { - elseFallback = exprParser.parseExpression(); - } - final var body = parseBlock(); return new PbsAst.FunctionDecl( name.lexeme(), @@ -380,7 +419,6 @@ public final class PbsParser { returnSpec.kind, returnSpec.returnType, returnSpec.resultErrorType, - elseFallback, body, span(fnToken.start(), body.span().getEnd())); } @@ -407,10 +445,6 @@ public final class PbsParser { span(fnToken.start(), end)); } - private void parseMethodDeclarationInBody(final PbsToken fnToken) { - parseFunctionLike(fnToken, false); - } - /** * Parses `declare callback` declaration. */ @@ -463,9 +497,20 @@ public final class PbsParser { consume(PbsTokenKind.USING, "Expected 'using' in 'implements' declaration"); final var binderName = consume(PbsTokenKind.IDENTIFIER, "Expected binder name after 'using'"); + final var methods = new ArrayList(); long end; if (cursor.check(PbsTokenKind.LEFT_BRACE)) { - end = consumeBalancedBraces(cursor.advance()); + cursor.advance(); + while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { + if (!cursor.match(PbsTokenKind.FN)) { + report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, + "Implements body accepts only function declarations"); + cursor.advance(); + continue; + } + methods.add(parseFunctionLike(cursor.previous())); + } + end = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end implements body").end(); } else { report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected '{' to start 'implements' body"); @@ -476,6 +521,7 @@ public final class PbsParser { contractName.lexeme(), ownerName.lexeme(), binderName.lexeme(), + ReadOnlyList.wrap(methods), span(implementsToken.start(), end)); } @@ -501,13 +547,13 @@ public final class PbsParser { private ParsedReturnSpec parseCallableReturnSpec(final PbsToken anchorToken) { if (cursor.match(PbsTokenKind.ARROW)) { - return parseReturnSpecFromMarker(cursor.previous()); + return parseReturnSpecFromMarker(); } if (cursor.match(PbsTokenKind.COLON)) { report(cursor.previous(), ParseErrors.E_PARSE_INVALID_RETURN_ANNOTATION, "Return annotations must use '->' syntax"); - return parseReturnSpecFromMarker(cursor.previous()); + return parseReturnSpecFromMarker(); } final var inferredSpan = span(anchorToken.end(), anchorToken.end()); @@ -517,7 +563,7 @@ public final class PbsParser { null); } - private ParsedReturnSpec parseReturnSpecFromMarker(final PbsToken markerToken) { + private ParsedReturnSpec parseReturnSpecFromMarker() { if (cursor.match(PbsTokenKind.RESULT)) { final var resultToken = cursor.previous(); consume(PbsTokenKind.LESS, "Expected '<' after 'result'"); @@ -624,13 +670,56 @@ public final class PbsParser { * Parses a brace-delimited block. */ private PbsAst.Block parseBlock() { - final var leftBrace = consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start block"); + return parseBlock("Expected '{' to start block", true); + } + + private PbsAst.Block parseExpressionSurfaceBlock(final String message) { + return parseBlock(message, true); + } + + private PbsAst.Block parseBlock(final String message, final boolean allowTailExpression) { + final var leftBrace = consume(PbsTokenKind.LEFT_BRACE, message); final var statements = new ArrayList(); + PbsAst.Expression tailExpression = null; while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { - statements.add(parseStatement()); + if (startsStructuredStatement() || isAssignmentStatementStart()) { + statements.add(parseStatement()); + continue; + } + + final var expression = exprParser.parseExpression(); + if (cursor.match(PbsTokenKind.SEMICOLON)) { + statements.add(new PbsAst.ExpressionStatement( + expression, + span(expression.span().getStart(), cursor.previous().end()))); + continue; + } + + if (allowTailExpression && cursor.check(PbsTokenKind.RIGHT_BRACE)) { + tailExpression = expression; + break; + } + + report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected ';' after expression"); + if (!cursor.isAtEnd() && !cursor.check(PbsTokenKind.RIGHT_BRACE)) { + cursor.advance(); + } } final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end block"); - return new PbsAst.Block(ReadOnlyList.wrap(statements), span(leftBrace.start(), rightBrace.end())); + return new PbsAst.Block( + ReadOnlyList.wrap(statements), + tailExpression, + span(leftBrace.start(), rightBrace.end())); + } + + private boolean startsStructuredStatement() { + return cursor.check(PbsTokenKind.LET) + || cursor.check(PbsTokenKind.IF) + || cursor.check(PbsTokenKind.FOR) + || cursor.check(PbsTokenKind.WHILE) + || cursor.check(PbsTokenKind.BREAK) + || cursor.check(PbsTokenKind.CONTINUE) + || cursor.check(PbsTokenKind.RETURN); } /** @@ -878,16 +967,6 @@ public final class PbsParser { }; } - private void consumeBlockForMember(final String memberKind) { - if (!cursor.match(PbsTokenKind.LEFT_BRACE)) { - report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, - "Expected '{' to start " + memberKind + " body"); - consumeDeclarationTerminator(); - return; - } - consumeBalancedBraces(cursor.previous()); - } - private long consumeDeclarationTerminator() { var end = cursor.previous().end(); var parenDepth = 0; @@ -989,4 +1068,10 @@ public final class PbsParser { PbsAst.TypeRef returnType, PbsAst.TypeRef resultErrorType) { } + + private record StructBodyParse( + ReadOnlyList methods, + ReadOnlyList ctors, + long end) { + } } 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 2d9380d4..c6d0db44 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 @@ -8,6 +8,7 @@ import p.studio.compiler.source.identifiers.FileId; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class PbsExprParserTest { @@ -60,14 +61,20 @@ class PbsExprParserTest { assertTrue(diagnostics.isEmpty(), "Expression control-flow forms should parse cleanly"); final var body = ast.functions().getFirst().body().statements(); - assertInstanceOf(PbsAst.IfExpr.class, + final var ifExpr = assertInstanceOf(PbsAst.IfExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(0)).initializer()); - assertInstanceOf(PbsAst.SwitchExpr.class, + assertInstanceOf(PbsAst.IntLiteralExpr.class, ifExpr.thenBlock().tailExpression()); + final var elseBlock = assertInstanceOf(PbsAst.BlockExpr.class, ifExpr.elseExpression()).block(); + assertInstanceOf(PbsAst.IntLiteralExpr.class, elseBlock.tailExpression()); + + final var switchExpr = assertInstanceOf(PbsAst.SwitchExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(1)).initializer()); + assertInstanceOf(PbsAst.IntLiteralExpr.class, switchExpr.arms().getFirst().block().tailExpression()); final var handle = assertInstanceOf(PbsAst.HandleExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer()); assertEquals(2, handle.arms().size()); + assertInstanceOf(PbsAst.IntLiteralExpr.class, handle.arms().get(1).block().tailExpression()); } @Test @@ -88,14 +95,28 @@ class PbsExprParserTest { assertTrue(diagnostics.isEmpty(), "Expression control-flow forms with return blocks should parse cleanly"); final var body = ast.functions().getFirst().body().statements(); - assertInstanceOf(PbsAst.IfExpr.class, + final var ifExpr = assertInstanceOf(PbsAst.IfExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(0)).initializer()); - assertInstanceOf(PbsAst.SwitchExpr.class, + assertEquals(1, ifExpr.thenBlock().statements().size()); + assertInstanceOf(PbsAst.ReturnStatement.class, ifExpr.thenBlock().statements().getFirst()); + assertNull(ifExpr.thenBlock().tailExpression()); + final var elseBlock = assertInstanceOf(PbsAst.BlockExpr.class, ifExpr.elseExpression()).block(); + assertEquals(1, elseBlock.statements().size()); + assertInstanceOf(PbsAst.ReturnStatement.class, elseBlock.statements().getFirst()); + assertNull(elseBlock.tailExpression()); + + final var switchExpr = assertInstanceOf(PbsAst.SwitchExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(1)).initializer()); + assertEquals(1, switchExpr.arms().getFirst().block().statements().size()); + assertInstanceOf(PbsAst.ReturnStatement.class, switchExpr.arms().getFirst().block().statements().getFirst()); + assertNull(switchExpr.arms().getFirst().block().tailExpression()); final var handle = assertInstanceOf(PbsAst.HandleExpr.class, assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer()); assertEquals(2, handle.arms().size()); + assertEquals(1, handle.arms().get(1).block().statements().size()); + assertInstanceOf(PbsAst.ReturnStatement.class, handle.arms().get(1).block().statements().getFirst()); + assertNull(handle.arms().get(1).block().tailExpression()); } @Test diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java index c48440a3..b7b29b1c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java @@ -97,6 +97,8 @@ class PbsParserTest { final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1)); assertEquals(2, structDecl.fields().size()); assertTrue(structDecl.hasBody()); + assertEquals(1, structDecl.methods().size()); + assertEquals(1, structDecl.ctors().size()); final var contractDecl = assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2)); assertEquals(1, contractDecl.signatures().size()); @@ -115,7 +117,8 @@ class PbsParserTest { assertEquals("Err", callbackDecl.resultErrorType().name()); assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(7)); - assertInstanceOf(PbsAst.ImplementsDecl.class, ast.topDecls().get(8)); + final var implementsDecl = assertInstanceOf(PbsAst.ImplementsDecl.class, ast.topDecls().get(8)); + assertEquals(1, implementsDecl.methods().size()); } @Test @@ -164,6 +167,37 @@ class PbsParserTest { assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(2)); } + @Test + void shouldRejectFunctionElseFallbackSurface() { + final var source = """ + fn run() -> int else 1 { + return 1; + } + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + + PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.hasErrors(), "Function-level else fallback syntax is not valid PBS core syntax"); + } + + @Test + void shouldReportEnumMixedAndDuplicateCases() { + final var source = """ + declare enum Mixed(A, B = 1); + declare enum Duplicated(Idle = 0, Idle = 1, Run = 1); + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + + PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_INVALID_ENUM_FORM.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_LABEL.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_ID.name()))); + } + @Test void shouldRecoverWithInvalidDeclarationNode() { final var source = """