implements PR006.2

This commit is contained in:
bQUARKz 2026-03-05 12:27:31 +00:00
parent 0cf2e3e099
commit 42392f3d02
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
8 changed files with 259 additions and 73 deletions

View File

@ -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`.

View File

@ -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.

View File

@ -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<StructField> fields,
ReadOnlyList<FunctionDecl> methods,
ReadOnlyList<CtorDecl> ctors,
boolean hasBody,
Span span) implements TopDecl {
}
@ -140,6 +141,7 @@ public final class PbsAst {
String contractName,
String ownerName,
String binderName,
ReadOnlyList<FunctionDecl> methods,
Span span) implements TopDecl {
}
@ -154,6 +156,13 @@ public final class PbsAst {
Span span) {
}
public record CtorDecl(
String name,
ReadOnlyList<Parameter> parameters,
Block body,
Span span) {
}
public record TypeRef(
TypeRefKind kind,
String name,
@ -241,6 +250,7 @@ public final class PbsAst {
public record Block(
ReadOnlyList<Statement> statements,
Expression tailExpression,
Span span) {
}

View File

@ -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,

View File

@ -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) {

View File

@ -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<PbsAst.FunctionDecl>();
final var ctors = new ArrayList<PbsAst.CtorDecl>();
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<PbsAst.StructField> 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<PbsAst.FunctionDecl>();
final var ctors = new ArrayList<PbsAst.CtorDecl>();
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<PbsAst.EnumCase>();
final var caseLabels = new HashSet<String>();
final var explicitCaseIds = new HashSet<Long>();
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<PbsAst.FunctionDecl>();
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.Statement>();
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<PbsAst.FunctionDecl> methods,
ReadOnlyList<PbsAst.CtorDecl> ctors,
long end) {
}
}

View File

@ -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

View File

@ -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 = """