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` 4. `PR-004-pbs-parser-statements-and-control-flow.md`
5. `PR-005-pbs-parser-expressions-optional-result-apply.md` 5. `PR-005-pbs-parser-expressions-optional-result-apply.md`
6. `PR-006-pbs-barrel-and-module-visibility.md` 6. `PR-006-pbs-barrel-and-module-visibility.md`
7. `PR-007-pbs-static-semantics-declaration-validation.md` 7. `PR-006.2-pbs-parser-ast-syntax-hardening.md`
8. `PR-008-pbs-static-semantics-call-resolution-and-flow.md` 8. `PR-006.3-pbs-syntax-completeness-and-module-hygiene.md`
9. `PR-009-pbs-diagnostics-contract-v1.md` 9. `PR-007-pbs-static-semantics-declaration-validation.md`
10. `PR-010-pbs-irbackend-lowering-contract.md` 10. `PR-008-pbs-static-semantics-call-resolution-and-flow.md`
11. `PR-011-pbs-gate-u-conformance-fixtures.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`. 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, ReturnKind returnKind,
TypeRef returnType, TypeRef returnType,
TypeRef resultErrorType, TypeRef resultErrorType,
Expression elseFallback,
Block body, Block body,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
@ -92,6 +91,8 @@ public final class PbsAst {
public record StructDecl( public record StructDecl(
String name, String name,
ReadOnlyList<StructField> fields, ReadOnlyList<StructField> fields,
ReadOnlyList<FunctionDecl> methods,
ReadOnlyList<CtorDecl> ctors,
boolean hasBody, boolean hasBody,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
@ -140,6 +141,7 @@ public final class PbsAst {
String contractName, String contractName,
String ownerName, String ownerName,
String binderName, String binderName,
ReadOnlyList<FunctionDecl> methods,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
@ -154,6 +156,13 @@ public final class PbsAst {
Span span) { Span span) {
} }
public record CtorDecl(
String name,
ReadOnlyList<Parameter> parameters,
Block body,
Span span) {
}
public record TypeRef( public record TypeRef(
TypeRefKind kind, TypeRefKind kind,
String name, String name,
@ -241,6 +250,7 @@ public final class PbsAst {
public record Block( public record Block(
ReadOnlyList<Statement> statements, ReadOnlyList<Statement> statements,
Expression tailExpression,
Span span) { Span span) {
} }

View File

@ -12,6 +12,9 @@ public enum ParseErrors {
E_PARSE_INVALID_ASSIGN_TARGET, E_PARSE_INVALID_ASSIGN_TARGET,
E_PARSE_INVALID_FOR_FORM, E_PARSE_INVALID_FOR_FORM,
E_PARSE_LOOP_CONTROL_OUTSIDE_LOOP, 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_SWITCH_FORM,
E_PARSE_INVALID_HANDLE_FORM, E_PARSE_INVALID_HANDLE_FORM,
E_PARSE_INVALID_TUPLE_LITERAL, E_PARSE_INVALID_TUPLE_LITERAL,

View File

@ -14,17 +14,25 @@ import java.util.ArrayList;
* Dedicated expression parser for PBS. * Dedicated expression parser for PBS.
*/ */
final class PbsExprParser { final class PbsExprParser {
@FunctionalInterface
interface BlockParserDelegate {
PbsAst.Block parse(String message);
}
private final PbsTokenCursor cursor; private final PbsTokenCursor cursor;
private final FileId fileId; private final FileId fileId;
private final DiagnosticSink diagnostics; private final DiagnosticSink diagnostics;
private final BlockParserDelegate blockParserDelegate;
PbsExprParser( PbsExprParser(
final PbsTokenCursor cursor, final PbsTokenCursor cursor,
final FileId fileId, final FileId fileId,
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics,
final BlockParserDelegate blockParserDelegate) {
this.cursor = cursor; this.cursor = cursor;
this.fileId = fileId; this.fileId = fileId;
this.diagnostics = diagnostics; this.diagnostics = diagnostics;
this.blockParserDelegate = blockParserDelegate;
} }
/** /**
@ -592,26 +600,7 @@ final class PbsExprParser {
} }
private PbsAst.Block parseSurfaceBlock(final String message) { private PbsAst.Block parseSurfaceBlock(final String message) {
final var open = consume(PbsTokenKind.LEFT_BRACE, message); return blockParserDelegate.parse(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));
} }
private PbsToken consume(final PbsTokenKind kind, final String 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 p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
/** /**
* High-level manual parser for PBS source files. * High-level manual parser for PBS source files.
@ -29,7 +30,7 @@ public final class PbsParser {
final FileId fileId, final FileId fileId,
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics) {
this.cursor = new PbsTokenCursor(tokens); 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.fileId = fileId;
this.diagnostics = diagnostics; this.diagnostics = diagnostics;
} }
@ -205,6 +206,8 @@ public final class PbsParser {
final var fields = parseStructFields(); final var fields = parseStructFields();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after struct fields"); 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; final boolean hasBody;
long end; long end;
if (cursor.match(PbsTokenKind.SEMICOLON)) { if (cursor.match(PbsTokenKind.SEMICOLON)) {
@ -212,7 +215,10 @@ public final class PbsParser {
end = cursor.previous().end(); end = cursor.previous().end();
} else if (cursor.match(PbsTokenKind.LEFT_BRACE)) { } else if (cursor.match(PbsTokenKind.LEFT_BRACE)) {
hasBody = true; 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 { } else {
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"Struct declaration must end with ';' or contain a '{...}' body"); "Struct declaration must end with ';' or contain a '{...}' body");
@ -220,7 +226,13 @@ public final class PbsParser {
end = consumeDeclarationTerminator(); 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() { private ArrayList<PbsAst.StructField> parseStructFields() {
@ -258,14 +270,16 @@ public final class PbsParser {
return fields; 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()) { while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
if (cursor.match(PbsTokenKind.FN)) { if (cursor.match(PbsTokenKind.FN)) {
parseMethodDeclarationInBody(cursor.previous()); methods.add(parseFunctionLike(cursor.previous()));
continue; continue;
} }
if (cursor.match(PbsTokenKind.CTOR)) { if (cursor.match(PbsTokenKind.CTOR)) {
parseCtorDeclarationInBody(cursor.previous()); ctors.add(parseCtorDeclarationInBody(cursor.previous()));
continue; continue;
} }
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
@ -273,15 +287,20 @@ public final class PbsParser {
cursor.advance(); cursor.advance();
} }
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end struct body"); 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) { private PbsAst.CtorDecl parseCtorDeclarationInBody(final PbsToken ctorToken) {
consume(PbsTokenKind.IDENTIFIER, "Expected constructor name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected constructor name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after constructor name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after constructor name");
parseParametersUntilRightParen(); final var parameters = parseParametersUntilRightParen();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after constructor parameters"); 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) { private PbsAst.ContractDecl parseContractDeclaration(final PbsToken declareToken) {
@ -312,7 +331,7 @@ public final class PbsParser {
cursor.advance(); cursor.advance();
continue; continue;
} }
methods.add(parseFunctionLike(cursor.previous(), false)); methods.add(parseFunctionLike(cursor.previous()));
} }
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end service body"); 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())); 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"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after enum name");
final var cases = new ArrayList<PbsAst.EnumCase>(); 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)) { if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
do { do {
final var caseName = consume(PbsTokenKind.IDENTIFIER, "Expected enum case label"); final var caseName = consume(PbsTokenKind.IDENTIFIER, "Expected enum case label");
@ -343,11 +366,32 @@ public final class PbsParser {
if (cursor.match(PbsTokenKind.EQUAL)) { if (cursor.match(PbsTokenKind.EQUAL)) {
final var intToken = consume(PbsTokenKind.INT_LITERAL, "Expected integer literal after '=' in enum case"); final var intToken = consume(PbsTokenKind.INT_LITERAL, "Expected integer literal after '=' in enum case");
explicitValue = parseLongOrNull(intToken.lexeme()); 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()))); cases.add(new PbsAst.EnumCase(caseName.lexeme(), explicitValue, span(caseName.start(), cursor.previous().end())));
} while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN)); } 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 rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after enum cases");
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after enum declaration"); 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()))); 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. * Parses a top-level function declaration.
*/ */
private PbsAst.FunctionDecl parseFunction(final PbsToken fnToken) { 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"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
final var parameters = parseParametersUntilRightParen(); final var parameters = parseParametersUntilRightParen();
@ -368,11 +412,6 @@ public final class PbsParser {
final var returnSpec = parseCallableReturnSpec(rightParen); final var returnSpec = parseCallableReturnSpec(rightParen);
PbsAst.Expression elseFallback = null;
if (allowElseFallback && cursor.match(PbsTokenKind.ELSE)) {
elseFallback = exprParser.parseExpression();
}
final var body = parseBlock(); final var body = parseBlock();
return new PbsAst.FunctionDecl( return new PbsAst.FunctionDecl(
name.lexeme(), name.lexeme(),
@ -380,7 +419,6 @@ public final class PbsParser {
returnSpec.kind, returnSpec.kind,
returnSpec.returnType, returnSpec.returnType,
returnSpec.resultErrorType, returnSpec.resultErrorType,
elseFallback,
body, body,
span(fnToken.start(), body.span().getEnd())); span(fnToken.start(), body.span().getEnd()));
} }
@ -407,10 +445,6 @@ public final class PbsParser {
span(fnToken.start(), end)); span(fnToken.start(), end));
} }
private void parseMethodDeclarationInBody(final PbsToken fnToken) {
parseFunctionLike(fnToken, false);
}
/** /**
* Parses `declare callback` declaration. * Parses `declare callback` declaration.
*/ */
@ -463,9 +497,20 @@ public final class PbsParser {
consume(PbsTokenKind.USING, "Expected 'using' in 'implements' declaration"); consume(PbsTokenKind.USING, "Expected 'using' in 'implements' declaration");
final var binderName = consume(PbsTokenKind.IDENTIFIER, "Expected binder name after 'using'"); final var binderName = consume(PbsTokenKind.IDENTIFIER, "Expected binder name after 'using'");
final var methods = new ArrayList<PbsAst.FunctionDecl>();
long end; long end;
if (cursor.check(PbsTokenKind.LEFT_BRACE)) { 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 { } else {
report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN,
"Expected '{' to start 'implements' body"); "Expected '{' to start 'implements' body");
@ -476,6 +521,7 @@ public final class PbsParser {
contractName.lexeme(), contractName.lexeme(),
ownerName.lexeme(), ownerName.lexeme(),
binderName.lexeme(), binderName.lexeme(),
ReadOnlyList.wrap(methods),
span(implementsToken.start(), end)); span(implementsToken.start(), end));
} }
@ -501,13 +547,13 @@ public final class PbsParser {
private ParsedReturnSpec parseCallableReturnSpec(final PbsToken anchorToken) { private ParsedReturnSpec parseCallableReturnSpec(final PbsToken anchorToken) {
if (cursor.match(PbsTokenKind.ARROW)) { if (cursor.match(PbsTokenKind.ARROW)) {
return parseReturnSpecFromMarker(cursor.previous()); return parseReturnSpecFromMarker();
} }
if (cursor.match(PbsTokenKind.COLON)) { if (cursor.match(PbsTokenKind.COLON)) {
report(cursor.previous(), ParseErrors.E_PARSE_INVALID_RETURN_ANNOTATION, report(cursor.previous(), ParseErrors.E_PARSE_INVALID_RETURN_ANNOTATION,
"Return annotations must use '->' syntax"); "Return annotations must use '->' syntax");
return parseReturnSpecFromMarker(cursor.previous()); return parseReturnSpecFromMarker();
} }
final var inferredSpan = span(anchorToken.end(), anchorToken.end()); final var inferredSpan = span(anchorToken.end(), anchorToken.end());
@ -517,7 +563,7 @@ public final class PbsParser {
null); null);
} }
private ParsedReturnSpec parseReturnSpecFromMarker(final PbsToken markerToken) { private ParsedReturnSpec parseReturnSpecFromMarker() {
if (cursor.match(PbsTokenKind.RESULT)) { if (cursor.match(PbsTokenKind.RESULT)) {
final var resultToken = cursor.previous(); final var resultToken = cursor.previous();
consume(PbsTokenKind.LESS, "Expected '<' after 'result'"); consume(PbsTokenKind.LESS, "Expected '<' after 'result'");
@ -624,13 +670,56 @@ public final class PbsParser {
* Parses a brace-delimited block. * Parses a brace-delimited block.
*/ */
private PbsAst.Block parseBlock() { 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>(); final var statements = new ArrayList<PbsAst.Statement>();
PbsAst.Expression tailExpression = null;
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { 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"); 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() { private long consumeDeclarationTerminator() {
var end = cursor.previous().end(); var end = cursor.previous().end();
var parenDepth = 0; var parenDepth = 0;
@ -989,4 +1068,10 @@ public final class PbsParser {
PbsAst.TypeRef returnType, PbsAst.TypeRef returnType,
PbsAst.TypeRef resultErrorType) { 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsExprParserTest { class PbsExprParserTest {
@ -60,14 +61,20 @@ class PbsExprParserTest {
assertTrue(diagnostics.isEmpty(), "Expression control-flow forms should parse cleanly"); assertTrue(diagnostics.isEmpty(), "Expression control-flow forms should parse cleanly");
final var body = ast.functions().getFirst().body().statements(); 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.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.LetStatement.class, body.get(1)).initializer());
assertInstanceOf(PbsAst.IntLiteralExpr.class, switchExpr.arms().getFirst().block().tailExpression());
final var handle = assertInstanceOf(PbsAst.HandleExpr.class, final var handle = assertInstanceOf(PbsAst.HandleExpr.class,
assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer()); assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer());
assertEquals(2, handle.arms().size()); assertEquals(2, handle.arms().size());
assertInstanceOf(PbsAst.IntLiteralExpr.class, handle.arms().get(1).block().tailExpression());
} }
@Test @Test
@ -88,14 +95,28 @@ class PbsExprParserTest {
assertTrue(diagnostics.isEmpty(), "Expression control-flow forms with return blocks should parse cleanly"); assertTrue(diagnostics.isEmpty(), "Expression control-flow forms with return blocks should parse cleanly");
final var body = ast.functions().getFirst().body().statements(); 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.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()); 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, final var handle = assertInstanceOf(PbsAst.HandleExpr.class,
assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer()); assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer());
assertEquals(2, handle.arms().size()); 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 @Test

View File

@ -97,6 +97,8 @@ class PbsParserTest {
final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1)); final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1));
assertEquals(2, structDecl.fields().size()); assertEquals(2, structDecl.fields().size());
assertTrue(structDecl.hasBody()); assertTrue(structDecl.hasBody());
assertEquals(1, structDecl.methods().size());
assertEquals(1, structDecl.ctors().size());
final var contractDecl = assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2)); final var contractDecl = assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2));
assertEquals(1, contractDecl.signatures().size()); assertEquals(1, contractDecl.signatures().size());
@ -115,7 +117,8 @@ class PbsParserTest {
assertEquals("Err", callbackDecl.resultErrorType().name()); assertEquals("Err", callbackDecl.resultErrorType().name());
assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(7)); 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 @Test
@ -164,6 +167,37 @@ class PbsParserTest {
assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(2)); 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 @Test
void shouldRecoverWithInvalidDeclarationNode() { void shouldRecoverWithInvalidDeclarationNode() {
final var source = """ final var source = """