implements PR003

This commit is contained in:
bQUARKz 2026-03-05 10:24:11 +00:00
parent 9ddfa19bf2
commit 96c5505c04
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
9 changed files with 531 additions and 137 deletions

View File

@ -1,35 +0,0 @@
# PR-002 - PBS AST Contract V1
## Briefing
Hoje o AST representa apenas funcoes, `let/return` e expressoes basicas.
A spec exige familias obrigatorias de declaracoes e metadados estaveis para parser, diagnosticos e lowering.
Este PR fecha o contrato estrutural do AST sem ainda resolver todas as regras semanticas.
## Target
- Specs:
- `docs/pbs/specs/11. AST Specification.md` (secoes 6, 7, 8, 9, 10)
- `docs/pbs/specs/3. Core Syntax Specification.md` (secoes 6, 7, 8, 9, 10)
- Codigo:
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java`
## Method
1. Remodelar o root para conter imports e uma lista de `TopDecl` em ordem de fonte.
2. Introduzir familias obrigatorias de declaracao (`fn`, `struct`, `contract`, `service`, `error`, `enum`, `callback`, `declare const`, `implements`).
3. Adicionar estruturas de tipo (unit, simple, optional, tuple nomeada, `Self`) e retorno (`plain`, `result`).
4. Garantir `file/start/end` em todos os nos consumidos por diagnostico/lowering.
5. Definir nos de erro/recovery explicitos para evitar AST permissivo.
## Acceptance Criteria
- Existe exatamente um root por arquivo com filhos em ordem deterministica.
- AST preserva identidade de declaracoes sem colapsar overload cedo.
- Cada declaracao obrigatoria possui nome, assinatura/superficie e span.
- Parser recovery nao fabrica semantica valida; erros ficam marcados no AST.
- Precedencia e associatividade continuam observaveis pelo shape da arvore.
## Tests
- `PbsParserTest`:
- validacao de shape do root (`imports + topDecls`);
- cobertura de todas as familias de declaracao obrigatorias no AST;
- asserts de spans (`file/start/end`) em nos principais;
- fixture com erro e recovery garantindo estrutura coerente.

View File

@ -1,35 +0,0 @@
# PR-003 - PBS Parser Declarations and Type Surfaces
## Briefing
A sintaxe de declaracoes e tipos da spec e significativamente maior que o parser atual.
Este PR implementa a fatia de parser para declaracoes top-level e superficies de tipo/retorno, mantendo rejeicao deterministica das formas reservadas fora do contexto permitido.
## Target
- Specs:
- `docs/pbs/specs/3. Core Syntax Specification.md` (secoes 6.2, 7.1-7.6, 8)
- `docs/pbs/specs/11. AST Specification.md` (secao 8)
- Codigo:
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java`
## Method
1. Implementar parser de `declare struct/service/contract/error/enum/callback/const` e `implements`.
2. Trocar assinatura de funcao para retorno via `->` com normalizacao de unit e single-slot.
3. Implementar parser de `TypeRef` com `optional`, tuple nomeada e `Self`.
4. Rejeitar no parser formas reservadas de `declare host` e `declare builtin type` em modulos ordinarios.
5. Melhorar sincronizacao de recovery por contexto de declaracao.
## Acceptance Criteria
- Top-level parseia todas as declaracoes previstas em `TopDecl`.
- Retornos seguem formato normativo (`->`, `result<Err>`, `void`/`()`).
- `optional` e tuple nomeada em tipo sao parseados com shape correto.
- Formas invalidas de declaracao geram erro deterministico e parser continua.
- `declare host`/`declare builtin type` em codigo comum sao rejeitados explicitamente.
## Tests
- `PbsParserDeclarationsTest` novo cobrindo:
- cada declaracao valida (struct, service, contract, error, enum, callback, const, implements);
- erros de shape (enum misto implicito/explicito, retorno mal formado, optional invalido);
- rejeicao de `declare host` fora de superficie reservada;
- spans e recovery em declaracoes quebradas.

View File

@ -49,7 +49,7 @@ public final class PbsFrontendCompiler {
fileId, fileId,
fn.name(), fn.name(),
fn.parameters().size(), fn.parameters().size(),
fn.returnType() != null, fn.returnKind() != PbsAst.ReturnKind.INFERRED_UNIT,
fn.span())); fn.span()));
} }
return ReadOnlyList.wrap(functions); return ReadOnlyList.wrap(functions);

View File

@ -9,6 +9,23 @@ public final class PbsAst {
private PbsAst() { private PbsAst() {
} }
public enum ReturnKind {
INFERRED_UNIT,
EXPLICIT_UNIT,
PLAIN,
RESULT
}
public enum TypeRefKind {
SIMPLE,
SELF,
OPTIONAL,
UNIT,
GROUP,
NAMED_TUPLE,
ERROR
}
public record File( public record File(
ReadOnlyList<ImportDecl> imports, ReadOnlyList<ImportDecl> imports,
ReadOnlyList<TopDecl> topDecls, ReadOnlyList<TopDecl> topDecls,
@ -59,7 +76,9 @@ public final class PbsAst {
public record FunctionDecl( public record FunctionDecl(
String name, String name,
ReadOnlyList<Parameter> parameters, ReadOnlyList<Parameter> parameters,
ReturnKind returnKind,
TypeRef returnType, TypeRef returnType,
TypeRef resultErrorType,
Expression elseFallback, Expression elseFallback,
Block body, Block body,
Span span) implements TopDecl { Span span) implements TopDecl {
@ -67,33 +86,41 @@ public final class PbsAst {
public record StructDecl( public record StructDecl(
String name, String name,
ReadOnlyList<StructField> fields,
boolean hasBody,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record ContractDecl( public record ContractDecl(
String name, String name,
ReadOnlyList<FunctionSignature> signatures,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record ServiceDecl( public record ServiceDecl(
String name, String name,
ReadOnlyList<FunctionDecl> methods,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record ErrorDecl( public record ErrorDecl(
String name, String name,
ReadOnlyList<String> cases,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record EnumDecl( public record EnumDecl(
String name, String name,
ReadOnlyList<EnumCase> cases,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record CallbackDecl( public record CallbackDecl(
String name, String name,
ReadOnlyList<Parameter> parameters, ReadOnlyList<Parameter> parameters,
ReturnKind returnKind,
TypeRef returnType, TypeRef returnType,
TypeRef resultErrorType,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
@ -123,7 +150,66 @@ public final class PbsAst {
} }
public record TypeRef( public record TypeRef(
TypeRefKind kind,
String name, String name,
ReadOnlyList<NamedTypeField> fields,
TypeRef inner,
Span span) {
public static TypeRef simple(final String name, final Span span) {
return new TypeRef(TypeRefKind.SIMPLE, name, ReadOnlyList.empty(), null, span);
}
public static TypeRef self(final Span span) {
return new TypeRef(TypeRefKind.SELF, "Self", ReadOnlyList.empty(), null, span);
}
public static TypeRef optional(final TypeRef inner, final Span span) {
return new TypeRef(TypeRefKind.OPTIONAL, "optional", ReadOnlyList.empty(), inner, span);
}
public static TypeRef unit(final Span span) {
return new TypeRef(TypeRefKind.UNIT, "void", ReadOnlyList.empty(), null, span);
}
public static TypeRef group(final TypeRef inner, final Span span) {
return new TypeRef(TypeRefKind.GROUP, "group", ReadOnlyList.empty(), inner, span);
}
public static TypeRef namedTuple(final ReadOnlyList<NamedTypeField> fields, final Span span) {
return new TypeRef(TypeRefKind.NAMED_TUPLE, "tuple", fields, null, span);
}
public static TypeRef error(final Span span) {
return new TypeRef(TypeRefKind.ERROR, "<error>", ReadOnlyList.empty(), null, span);
}
}
public record NamedTypeField(
String label,
TypeRef typeRef,
Span span) {
}
public record StructField(
String name,
TypeRef typeRef,
boolean isPublic,
boolean isMutable,
Span span) {
}
public record FunctionSignature(
String name,
ReadOnlyList<Parameter> parameters,
ReturnKind returnKind,
TypeRef returnType,
TypeRef resultErrorType,
Span span) {
}
public record EnumCase(
String name,
Long explicitValue,
Span span) { Span span) {
} }

View File

@ -5,4 +5,8 @@ public enum ParseErrors {
E_PARSE_UNEXPECTED_TOKEN, E_PARSE_UNEXPECTED_TOKEN,
E_PARSE_VISIBILITY_IN_SOURCE, E_PARSE_VISIBILITY_IN_SOURCE,
E_PARSE_NON_ASSOC, E_PARSE_NON_ASSOC,
E_PARSE_INVALID_DECL_SHAPE,
E_PARSE_INVALID_RETURN_ANNOTATION,
E_PARSE_INVALID_TYPE_SURFACE,
E_PARSE_RESERVED_DECLARATION,
} }

View File

@ -157,19 +157,19 @@ public final class PbsParser {
*/ */
private PbsAst.TopDecl parseDeclareTopDecl(final PbsToken declareToken) { private PbsAst.TopDecl parseDeclareTopDecl(final PbsToken declareToken) {
if (cursor.match(PbsTokenKind.STRUCT)) { if (cursor.match(PbsTokenKind.STRUCT)) {
return parseSimpleNamedDecl(declareToken, "struct", PbsAst.StructDecl::new); return parseStructDeclaration(declareToken);
} }
if (cursor.match(PbsTokenKind.CONTRACT)) { if (cursor.match(PbsTokenKind.CONTRACT)) {
return parseSimpleNamedDecl(declareToken, "contract", PbsAst.ContractDecl::new); return parseContractDeclaration(declareToken);
} }
if (cursor.match(PbsTokenKind.SERVICE)) { if (cursor.match(PbsTokenKind.SERVICE)) {
return parseSimpleNamedDecl(declareToken, "service", PbsAst.ServiceDecl::new); return parseServiceDeclaration(declareToken);
} }
if (cursor.match(PbsTokenKind.ERROR)) { if (cursor.match(PbsTokenKind.ERROR)) {
return parseSimpleNamedDecl(declareToken, "error", PbsAst.ErrorDecl::new); return parseErrorDeclaration(declareToken);
} }
if (cursor.match(PbsTokenKind.ENUM)) { if (cursor.match(PbsTokenKind.ENUM)) {
return parseSimpleNamedDecl(declareToken, "enum", PbsAst.EnumDecl::new); return parseEnumDeclaration(declareToken);
} }
if (cursor.match(PbsTokenKind.CALLBACK)) { if (cursor.match(PbsTokenKind.CALLBACK)) {
return parseCallbackDeclaration(declareToken); return parseCallbackDeclaration(declareToken);
@ -179,14 +179,14 @@ public final class PbsParser {
} }
if (cursor.match(PbsTokenKind.HOST)) { if (cursor.match(PbsTokenKind.HOST)) {
final var end = consumeDeclarationTerminator(); final var end = consumeDeclarationTerminator();
report(cursor.previous(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, report(cursor.previous(), ParseErrors.E_PARSE_RESERVED_DECLARATION,
"'declare host' is reserved and not supported in ordinary source modules"); "'declare host' is reserved and not supported in ordinary source modules");
return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end)); return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end));
} }
if (cursor.match(PbsTokenKind.BUILTIN)) { if (cursor.match(PbsTokenKind.BUILTIN)) {
cursor.match(PbsTokenKind.TYPE); consume(PbsTokenKind.TYPE, "Expected 'type' in 'declare builtin type' declaration");
final var end = consumeDeclarationTerminator(); final var end = consumeDeclarationTerminator();
report(cursor.previous(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, report(cursor.previous(), ParseErrors.E_PARSE_RESERVED_DECLARATION,
"'declare builtin type' is reserved and not supported in ordinary source modules"); "'declare builtin type' is reserved and not supported in ordinary source modules");
return new PbsAst.InvalidDecl("reserved declare builtin type", span(declareToken.start(), end)); return new PbsAst.InvalidDecl("reserved declare builtin type", span(declareToken.start(), end));
} }
@ -198,22 +198,177 @@ public final class PbsParser {
return new PbsAst.InvalidDecl("invalid declare form", span(declareToken.start(), token.end())); return new PbsAst.InvalidDecl("invalid declare form", span(declareToken.start(), token.end()));
} }
private PbsAst.StructDecl parseStructDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected struct name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in struct declaration");
final var fields = parseStructFields();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after struct fields");
final boolean hasBody;
long end;
if (cursor.match(PbsTokenKind.SEMICOLON)) {
hasBody = false;
end = cursor.previous().end();
} else if (cursor.match(PbsTokenKind.LEFT_BRACE)) {
hasBody = true;
end = parseStructBodyAndConsumeRightBrace(cursor.previous());
} else {
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"Struct declaration must end with ';' or contain a '{...}' body");
hasBody = false;
end = consumeDeclarationTerminator();
}
return new PbsAst.StructDecl(name.lexeme(), ReadOnlyList.wrap(fields), hasBody, span(declareToken.start(), end));
}
private ArrayList<PbsAst.StructField> parseStructFields() {
final var fields = new ArrayList<PbsAst.StructField>();
if (cursor.check(PbsTokenKind.RIGHT_PAREN)) {
return fields;
}
do {
final var start = cursor.peek();
var isPublic = false;
var isMutable = false;
if (cursor.match(PbsTokenKind.PUB)) {
isPublic = true;
if (cursor.match(PbsTokenKind.MUT)) {
isMutable = true;
}
} else if (cursor.match(PbsTokenKind.MUT)) {
final var mutToken = cursor.previous();
report(mutToken, ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"'mut' is only valid immediately after 'pub' in struct fields");
}
final var fieldName = consume(PbsTokenKind.IDENTIFIER, "Expected struct field name");
consume(PbsTokenKind.COLON, "Expected ':' after struct field name");
final var typeRef = parseTypeRef();
fields.add(new PbsAst.StructField(
fieldName.lexeme(),
typeRef,
isPublic,
isMutable,
span(start.start(), typeRef.span().getEnd())));
} while (cursor.match(PbsTokenKind.COMMA));
return fields;
}
private long parseStructBodyAndConsumeRightBrace(final PbsToken leftBrace) {
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
if (cursor.match(PbsTokenKind.FN)) {
parseMethodDeclarationInBody(cursor.previous());
continue;
}
if (cursor.match(PbsTokenKind.CTOR)) {
parseCtorDeclarationInBody(cursor.previous());
continue;
}
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"Struct body accepts only 'fn' methods and 'ctor' declarations");
cursor.advance();
}
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end struct body");
return rightBrace.end();
}
private void parseCtorDeclarationInBody(final PbsToken ctorToken) {
consume(PbsTokenKind.IDENTIFIER, "Expected constructor name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after constructor name");
parseParametersUntilRightParen();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after constructor parameters");
consumeBlockForMember("constructor");
}
private PbsAst.ContractDecl parseContractDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected contract name");
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start contract body");
final var signatures = new ArrayList<PbsAst.FunctionSignature>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
if (!cursor.match(PbsTokenKind.FN)) {
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"Contract body accepts only function signatures");
cursor.advance();
continue;
}
signatures.add(parseFunctionSignature(cursor.previous(), true));
}
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end contract body");
return new PbsAst.ContractDecl(name.lexeme(), ReadOnlyList.wrap(signatures), span(declareToken.start(), rightBrace.end()));
}
private PbsAst.ServiceDecl parseServiceDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected service name");
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start service body");
final var methods = new ArrayList<PbsAst.FunctionDecl>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
if (!cursor.match(PbsTokenKind.FN)) {
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"Service body accepts only method declarations");
cursor.advance();
continue;
}
methods.add(parseFunctionLike(cursor.previous(), false));
}
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()));
}
private PbsAst.ErrorDecl parseErrorDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected error name");
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start error body");
final var cases = new ArrayList<String>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
final var caseToken = consume(PbsTokenKind.IDENTIFIER, "Expected error case label");
cases.add(caseToken.lexeme());
consume(PbsTokenKind.SEMICOLON, "Expected ';' after error case label");
}
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end error body");
return new PbsAst.ErrorDecl(name.lexeme(), ReadOnlyList.wrap(cases), span(declareToken.start(), rightBrace.end()));
}
private PbsAst.EnumDecl parseEnumDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected enum name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after enum name");
final var cases = new ArrayList<PbsAst.EnumCase>();
if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
do {
final var caseName = consume(PbsTokenKind.IDENTIFIER, "Expected enum case label");
Long explicitValue = null;
if (cursor.match(PbsTokenKind.EQUAL)) {
final var intToken = consume(PbsTokenKind.INT_LITERAL, "Expected integer literal after '=' in enum case");
explicitValue = parseLongOrNull(intToken.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));
}
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())));
}
/** /**
* 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);
}
private PbsAst.FunctionDecl parseFunctionLike(final PbsToken fnToken, final boolean allowElseFallback) {
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();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list"); final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list");
PbsAst.TypeRef returnType = null; final var returnSpec = parseCallableReturnSpec(rightParen);
if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) {
returnType = parseTypeRef();
}
PbsAst.Expression elseFallback = null; PbsAst.Expression elseFallback = null;
if (cursor.match(PbsTokenKind.ELSE)) { if (allowElseFallback && cursor.match(PbsTokenKind.ELSE)) {
elseFallback = exprParser.parseExpression(); elseFallback = exprParser.parseExpression();
} }
@ -221,12 +376,40 @@ public final class PbsParser {
return new PbsAst.FunctionDecl( return new PbsAst.FunctionDecl(
name.lexeme(), name.lexeme(),
ReadOnlyList.wrap(parameters), ReadOnlyList.wrap(parameters),
returnType, returnSpec.kind,
returnSpec.returnType,
returnSpec.resultErrorType,
elseFallback, elseFallback,
body, body,
span(fnToken.start(), body.span().getEnd())); span(fnToken.start(), body.span().getEnd()));
} }
private PbsAst.FunctionSignature parseFunctionSignature(final PbsToken fnToken, final boolean requireSemicolon) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
final var parameters = parseParametersUntilRightParen();
final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list");
final var returnSpec = parseCallableReturnSpec(rightParen);
long end = returnSpec.returnType.span().getEnd();
if (requireSemicolon) {
end = consume(PbsTokenKind.SEMICOLON, "Expected ';' after function signature").end();
}
return new PbsAst.FunctionSignature(
name.lexeme(),
ReadOnlyList.wrap(parameters),
returnSpec.kind,
returnSpec.returnType,
returnSpec.resultErrorType,
span(fnToken.start(), end));
}
private void parseMethodDeclarationInBody(final PbsToken fnToken) {
parseFunctionLike(fnToken, false);
}
/** /**
* Parses `declare callback` declaration. * Parses `declare callback` declaration.
*/ */
@ -234,18 +417,17 @@ public final class PbsParser {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected callback name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected callback name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name");
final var parameters = parseParametersUntilRightParen(); final var parameters = parseParametersUntilRightParen();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after callback parameter list"); final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after callback parameter list");
PbsAst.TypeRef returnType = null; final var returnSpec = parseCallableReturnSpec(rightParen);
if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) {
returnType = parseTypeRef();
}
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after callback declaration"); final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after callback declaration");
return new PbsAst.CallbackDecl( return new PbsAst.CallbackDecl(
name.lexeme(), name.lexeme(),
ReadOnlyList.wrap(parameters), ReadOnlyList.wrap(parameters),
returnType, returnSpec.kind,
returnSpec.returnType,
returnSpec.resultErrorType,
span(declareToken.start(), semicolon.end())); span(declareToken.start(), semicolon.end()));
} }
@ -296,15 +478,6 @@ public final class PbsParser {
span(implementsToken.start(), end)); span(implementsToken.start(), end));
} }
private PbsAst.TopDecl parseSimpleNamedDecl(
final PbsToken declareToken,
final String kind,
final NamedDeclFactory factory) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected " + kind + " name");
final var end = consumeDeclarationTerminator();
return factory.build(name.lexeme(), span(declareToken.start(), end));
}
private ArrayList<PbsAst.Parameter> parseParametersUntilRightParen() { private ArrayList<PbsAst.Parameter> parseParametersUntilRightParen() {
final var parameters = new ArrayList<PbsAst.Parameter>(); final var parameters = new ArrayList<PbsAst.Parameter>();
if (cursor.check(PbsTokenKind.RIGHT_PAREN)) { if (cursor.check(PbsTokenKind.RIGHT_PAREN)) {
@ -325,21 +498,125 @@ public final class PbsParser {
return parameters; return parameters;
} }
private ParsedReturnSpec parseCallableReturnSpec(final PbsToken anchorToken) {
if (cursor.match(PbsTokenKind.ARROW)) {
return parseReturnSpecFromMarker(cursor.previous());
}
if (cursor.match(PbsTokenKind.COLON)) {
report(cursor.previous(), ParseErrors.E_PARSE_INVALID_RETURN_ANNOTATION,
"Return annotations must use '->' syntax");
return parseReturnSpecFromMarker(cursor.previous());
}
final var inferredSpan = span(anchorToken.end(), anchorToken.end());
return new ParsedReturnSpec(
PbsAst.ReturnKind.INFERRED_UNIT,
PbsAst.TypeRef.unit(inferredSpan),
null);
}
private ParsedReturnSpec parseReturnSpecFromMarker(final PbsToken markerToken) {
if (cursor.match(PbsTokenKind.RESULT)) {
final var resultToken = cursor.previous();
consume(PbsTokenKind.LESS, "Expected '<' after 'result'");
final var errorType = parseTypeRef();
consume(PbsTokenKind.GREATER, "Expected '>' after result error type");
final PbsAst.TypeRef payloadType;
if (isTypeStart(cursor.peek().kind())) {
payloadType = parseTypeRef();
} else {
payloadType = PbsAst.TypeRef.unit(span(resultToken.end(), resultToken.end()));
}
return new ParsedReturnSpec(
PbsAst.ReturnKind.RESULT,
payloadType,
errorType);
}
final var typeRef = parseTypeRef();
if (typeRef.kind() == PbsAst.TypeRefKind.UNIT) {
return new ParsedReturnSpec(PbsAst.ReturnKind.EXPLICIT_UNIT, typeRef, null);
}
return new ParsedReturnSpec(PbsAst.ReturnKind.PLAIN, typeRef, null);
}
/** /**
* Parses a simple identifier-based type reference such as {@code int} or {@code Vector}. * Parses type references including optional, unit, grouped, and named-tuple forms.
*/ */
private PbsAst.TypeRef parseTypeRef() { private PbsAst.TypeRef parseTypeRef() {
if (cursor.match(PbsTokenKind.IDENTIFIER, PbsTokenKind.SELF)) { if (cursor.match(PbsTokenKind.OPTIONAL)) {
final var identifier = cursor.previous(); final var optionalToken = cursor.previous();
return new PbsAst.TypeRef(identifier.lexeme(), span(identifier.start(), identifier.end())); final var inner = parseTypeRef();
if (inner.kind() == PbsAst.TypeRefKind.UNIT) {
report(optionalToken, ParseErrors.E_PARSE_INVALID_TYPE_SURFACE,
"'optional void' is not a valid type surface");
}
return PbsAst.TypeRef.optional(inner, span(optionalToken.start(), inner.span().getEnd()));
}
if (cursor.match(PbsTokenKind.VOID)) {
final var token = cursor.previous();
return PbsAst.TypeRef.unit(span(token.start(), token.end()));
}
if (cursor.match(PbsTokenKind.SELF)) {
final var token = cursor.previous();
return PbsAst.TypeRef.self(span(token.start(), token.end()));
}
if (cursor.match(PbsTokenKind.IDENTIFIER)) {
final var token = cursor.previous();
return PbsAst.TypeRef.simple(token.lexeme(), span(token.start(), token.end()));
}
if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
final var open = cursor.previous();
if (cursor.match(PbsTokenKind.RIGHT_PAREN)) {
return PbsAst.TypeRef.unit(span(open.start(), cursor.previous().end()));
}
if (cursor.check(PbsTokenKind.IDENTIFIER) && cursor.checkNext(PbsTokenKind.COLON)) {
final var fields = parseNamedTupleTypeFields();
final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after named tuple type");
return PbsAst.TypeRef.namedTuple(ReadOnlyList.wrap(fields), span(open.start(), close.end()));
}
final var inner = parseTypeRef();
final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after grouped type");
return PbsAst.TypeRef.group(inner, span(open.start(), close.end()));
} }
final var token = cursor.peek(); final var token = cursor.peek();
report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected type name"); report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected type surface");
if (!cursor.isAtEnd()) { if (!cursor.isAtEnd()) {
cursor.advance(); cursor.advance();
} }
return new PbsAst.TypeRef("<error>", span(token.start(), token.end())); return PbsAst.TypeRef.error(span(token.start(), token.end()));
}
private ArrayList<PbsAst.NamedTypeField> parseNamedTupleTypeFields() {
final var fields = new ArrayList<PbsAst.NamedTypeField>();
do {
final var label = consume(PbsTokenKind.IDENTIFIER, "Expected tuple field label");
consume(PbsTokenKind.COLON, "Expected ':' after tuple field label");
final var typeRef = parseTypeRef();
fields.add(new PbsAst.NamedTypeField(
label.lexeme(),
typeRef,
span(label.start(), typeRef.span().getEnd())));
} while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN));
return fields;
}
private boolean isTypeStart(final PbsTokenKind kind) {
return kind == PbsTokenKind.IDENTIFIER
|| kind == PbsTokenKind.SELF
|| kind == PbsTokenKind.OPTIONAL
|| kind == PbsTokenKind.VOID
|| kind == PbsTokenKind.LEFT_PAREN;
} }
/** /**
@ -412,6 +689,16 @@ public final class PbsParser {
return new PbsAst.ExpressionStatement(expression, span(expression.span().getStart(), semicolon.end())); return new PbsAst.ExpressionStatement(expression, span(expression.span().getStart(), semicolon.end()));
} }
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;
@ -486,6 +773,14 @@ public final class PbsParser {
return token; return token;
} }
private Long parseLongOrNull(final String text) {
try {
return Long.parseLong(text);
} catch (NumberFormatException ignored) {
return null;
}
}
/** /**
* Builds a source span for the current file. * Builds a source span for the current file.
*/ */
@ -500,8 +795,9 @@ public final class PbsParser {
diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end())); diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
} }
@FunctionalInterface private record ParsedReturnSpec(
private interface NamedDeclFactory { PbsAst.ReturnKind kind,
PbsAst.TopDecl build(String name, Span declarationSpan); PbsAst.TypeRef returnType,
PbsAst.TypeRef resultErrorType) {
} }
} }

View File

@ -74,4 +74,19 @@ final class PbsTokenCursor {
PbsToken previous() { PbsToken previous() {
return tokens.get(Math.max(current - 1, 0)); return tokens.get(Math.max(current - 1, 0));
} }
/**
* Returns the token at a positive lookahead offset from current token.
*/
PbsToken peek(final int offset) {
final var index = Math.min(current + Math.max(offset, 0), tokens.size() - 1);
return tokens.get(index);
}
/**
* Returns whether the token at lookahead offset 1 matches the expected kind.
*/
boolean checkNext(final PbsTokenKind kind) {
return peek(1).kind() == kind;
}
} }

View File

@ -12,11 +12,11 @@ class PbsFrontendCompilerTest {
@Test @Test
void shouldLowerFunctionsToIr() { void shouldLowerFunctionsToIr() {
final var source = """ final var source = """
fn a(): int { fn a() -> int {
return 1; return 1;
} }
fn b(x: int): int { fn b(x: int) -> int {
return x; return x;
} }
"""; """;
@ -37,8 +37,8 @@ class PbsFrontendCompilerTest {
@Test @Test
void shouldReportDuplicateFunctionNames() { void shouldReportDuplicateFunctionNames() {
final var source = """ final var source = """
fn a(): int { return 1; } fn a() -> int { return 1; }
fn a(): int { return 2; } fn a() -> int { return 2; }
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();

View File

@ -8,14 +8,15 @@ 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 PbsParserTest { class PbsParserTest {
@Test @Test
void shouldParseSingleFunction() { void shouldParseSingleFunctionWithArrowReturn() {
final var source = """ final var source = """
fn sum(a: int, b: int): int fn sum(a: int, b: int) -> int
{ {
return a + b; return a + b;
} }
@ -34,7 +35,9 @@ class PbsParserTest {
final var fn = ast.functions().getFirst(); final var fn = ast.functions().getFirst();
assertEquals("sum", fn.name()); assertEquals("sum", fn.name());
assertEquals(2, fn.parameters().size()); assertEquals(2, fn.parameters().size());
assertEquals(PbsAst.ReturnKind.PLAIN, fn.returnKind());
assertEquals("int", fn.returnType().name()); assertEquals("int", fn.returnType().name());
assertNull(fn.resultErrorType());
assertEquals(1, fn.body().statements().size()); assertEquals(1, fn.body().statements().size());
assertInstanceOf(PbsAst.ReturnStatement.class, fn.body().statements().getFirst()); assertInstanceOf(PbsAst.ReturnStatement.class, fn.body().statements().getFirst());
@ -43,13 +46,13 @@ class PbsParserTest {
} }
@Test @Test
void shouldPreserveImportsAndTopDeclOrdering() { void shouldParseImportsAndTopDeclOrdering() {
final var source = """ final var source = """
import { Vec2 as V2 } from @core:math; import { Vec2 as V2 } from @core:math;
fn first(): int { return 1; } fn first() -> int { return 1; }
declare struct Point(x: int, y: int); declare struct Point(x: int, y: int);
declare callback Tick(dt: int): int; declare callback Tick(dt: int) -> int;
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0); final var fileId = new FileId(0);
@ -70,17 +73,17 @@ class PbsParserTest {
} }
@Test @Test
void shouldRepresentMandatoryDeclarationFamilies() { void shouldParseDeclarationFamiliesWithShape() {
final var source = """ final var source = """
fn f(): int { return 1; } fn f() -> int { return 1; }
declare struct S(x: int); declare struct S(pub mut x: int, y: optional int) { fn run() -> void { return; } ctor make(x: int) { return; } }
declare contract C { fn run(x: int): int; } declare contract C { fn run(x: int) -> int; }
declare service Game { fn tick(x: int): int { return x; } } declare service Game { fn tick(x: int) -> int { return x; } }
declare error Err { Fail; } declare error Err { Fail; Crash; }
declare enum Mode(Idle, Run); declare enum Mode(Idle = 0, Run = 1);
declare callback TickCb(x: int): int; declare callback TickCb(x: int) -> result<Err> int;
declare const LIMIT: int = 10; declare const LIMIT: int = 10;
implements C for S using s { fn run(x: int): int { return x; } } implements C for S using s { fn run(x: int) -> int { return x; } }
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0); final var fileId = new FileId(0);
@ -88,24 +91,84 @@ class PbsParserTest {
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics); final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Parser should represent all mandatory declaration families"); assertTrue(diagnostics.isEmpty(), "Parser should represent declaration families with expected shapes");
assertEquals(9, ast.topDecls().size()); assertEquals(9, ast.topDecls().size());
assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(0));
assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1)); final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1));
assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2)); assertEquals(2, structDecl.fields().size());
assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().get(3)); assertTrue(structDecl.hasBody());
assertInstanceOf(PbsAst.ErrorDecl.class, ast.topDecls().get(4));
assertInstanceOf(PbsAst.EnumDecl.class, ast.topDecls().get(5)); final var contractDecl = assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2));
assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(6)); assertEquals(1, contractDecl.signatures().size());
final var serviceDecl = assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().get(3));
assertEquals(1, serviceDecl.methods().size());
final var errorDecl = assertInstanceOf(PbsAst.ErrorDecl.class, ast.topDecls().get(4));
assertEquals(2, errorDecl.cases().size());
final var enumDecl = assertInstanceOf(PbsAst.EnumDecl.class, ast.topDecls().get(5));
assertEquals(2, enumDecl.cases().size());
final var callbackDecl = assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(6));
assertEquals(PbsAst.ReturnKind.RESULT, callbackDecl.returnKind());
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)); assertInstanceOf(PbsAst.ImplementsDecl.class, ast.topDecls().get(8));
} }
@Test
void shouldParseOptionalAndNamedTupleTypes() {
final var source = """
fn shape(a: optional int, b: (x: int, y: optional float)) -> (value: optional int, meta: (k: int, v: float)) {
return a;
}
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final PbsAst.File ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Parser should accept optional and named tuple type surfaces");
final var fn = ast.functions().getFirst();
final var p0 = fn.parameters().get(0).typeRef();
assertEquals(PbsAst.TypeRefKind.OPTIONAL, p0.kind());
assertEquals(PbsAst.TypeRefKind.SIMPLE, p0.inner().kind());
final var p1 = fn.parameters().get(1).typeRef();
assertEquals(PbsAst.TypeRefKind.NAMED_TUPLE, p1.kind());
assertEquals(2, p1.fields().size());
final var returnType = fn.returnType();
assertEquals(PbsAst.TypeRefKind.NAMED_TUPLE, returnType.kind());
}
@Test
void shouldRejectReservedDeclareHostAndBuiltinType() {
final var source = """
declare host HostApi { fn run() -> int; }
declare builtin type Vec(x: int) { fn mag() -> float; }
fn ok() -> int { return 1; }
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final PbsAst.File ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);
assertTrue(diagnostics.hasErrors(), "Parser should reject reserved declare forms in ordinary source");
assertEquals(3, ast.topDecls().size());
assertInstanceOf(PbsAst.InvalidDecl.class, ast.topDecls().get(0));
assertInstanceOf(PbsAst.InvalidDecl.class, ast.topDecls().get(1));
assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(2));
}
@Test @Test
void shouldRecoverWithInvalidDeclarationNode() { void shouldRecoverWithInvalidDeclarationNode() {
final var source = """ final var source = """
declare ; declare ;
fn ok(): int { return 1; } fn ok() -> int { return 1; }
"""; """;
final var diagnostics = DiagnosticSink.empty(); final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0); final var fileId = new FileId(0);