diff --git a/docs/pbs/pull-requests/PR-002-pbs-ast-contract-v1.md b/docs/pbs/pull-requests/PR-002-pbs-ast-contract-v1.md deleted file mode 100644 index a0b82970..00000000 --- a/docs/pbs/pull-requests/PR-002-pbs-ast-contract-v1.md +++ /dev/null @@ -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. diff --git a/docs/pbs/pull-requests/PR-003-pbs-parser-declarations-and-types.md b/docs/pbs/pull-requests/PR-003-pbs-parser-declarations-and-types.md deleted file mode 100644 index 6f172d02..00000000 --- a/docs/pbs/pull-requests/PR-003-pbs-parser-declarations-and-types.md +++ /dev/null @@ -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`, `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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java index 7c0ca93c..d23348fe 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java @@ -49,7 +49,7 @@ public final class PbsFrontendCompiler { fileId, fn.name(), fn.parameters().size(), - fn.returnType() != null, + fn.returnKind() != PbsAst.ReturnKind.INFERRED_UNIT, fn.span())); } return ReadOnlyList.wrap(functions); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java index bcc55a36..429af7b2 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java @@ -9,6 +9,23 @@ public final class 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( ReadOnlyList imports, ReadOnlyList topDecls, @@ -59,7 +76,9 @@ public final class PbsAst { public record FunctionDecl( String name, ReadOnlyList parameters, + ReturnKind returnKind, TypeRef returnType, + TypeRef resultErrorType, Expression elseFallback, Block body, Span span) implements TopDecl { @@ -67,33 +86,41 @@ public final class PbsAst { public record StructDecl( String name, + ReadOnlyList fields, + boolean hasBody, Span span) implements TopDecl { } public record ContractDecl( String name, + ReadOnlyList signatures, Span span) implements TopDecl { } public record ServiceDecl( String name, + ReadOnlyList methods, Span span) implements TopDecl { } public record ErrorDecl( String name, + ReadOnlyList cases, Span span) implements TopDecl { } public record EnumDecl( String name, + ReadOnlyList cases, Span span) implements TopDecl { } public record CallbackDecl( String name, ReadOnlyList parameters, + ReturnKind returnKind, TypeRef returnType, + TypeRef resultErrorType, Span span) implements TopDecl { } @@ -123,7 +150,66 @@ public final class PbsAst { } public record TypeRef( + TypeRefKind kind, String name, + ReadOnlyList 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 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, "", 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 parameters, + ReturnKind returnKind, + TypeRef returnType, + TypeRef resultErrorType, + Span span) { + } + + public record EnumCase( + String name, + Long explicitValue, Span span) { } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java index e9fd9c3f..5fc957e2 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java @@ -5,4 +5,8 @@ public enum ParseErrors { E_PARSE_UNEXPECTED_TOKEN, E_PARSE_VISIBILITY_IN_SOURCE, E_PARSE_NON_ASSOC, + E_PARSE_INVALID_DECL_SHAPE, + E_PARSE_INVALID_RETURN_ANNOTATION, + E_PARSE_INVALID_TYPE_SURFACE, + E_PARSE_RESERVED_DECLARATION, } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java index 245272e5..72c8d120 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java @@ -157,19 +157,19 @@ public final class PbsParser { */ private PbsAst.TopDecl parseDeclareTopDecl(final PbsToken declareToken) { if (cursor.match(PbsTokenKind.STRUCT)) { - return parseSimpleNamedDecl(declareToken, "struct", PbsAst.StructDecl::new); + return parseStructDeclaration(declareToken); } if (cursor.match(PbsTokenKind.CONTRACT)) { - return parseSimpleNamedDecl(declareToken, "contract", PbsAst.ContractDecl::new); + return parseContractDeclaration(declareToken); } if (cursor.match(PbsTokenKind.SERVICE)) { - return parseSimpleNamedDecl(declareToken, "service", PbsAst.ServiceDecl::new); + return parseServiceDeclaration(declareToken); } if (cursor.match(PbsTokenKind.ERROR)) { - return parseSimpleNamedDecl(declareToken, "error", PbsAst.ErrorDecl::new); + return parseErrorDeclaration(declareToken); } if (cursor.match(PbsTokenKind.ENUM)) { - return parseSimpleNamedDecl(declareToken, "enum", PbsAst.EnumDecl::new); + return parseEnumDeclaration(declareToken); } if (cursor.match(PbsTokenKind.CALLBACK)) { return parseCallbackDeclaration(declareToken); @@ -179,14 +179,14 @@ public final class PbsParser { } if (cursor.match(PbsTokenKind.HOST)) { 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"); return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end)); } if (cursor.match(PbsTokenKind.BUILTIN)) { - cursor.match(PbsTokenKind.TYPE); + consume(PbsTokenKind.TYPE, "Expected 'type' in 'declare builtin type' declaration"); 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"); 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())); } + 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 parseStructFields() { + final var fields = new ArrayList(); + 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(); + 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(); + 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(); + 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(); + 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. */ 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"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); 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; - if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) { - returnType = parseTypeRef(); - } + final var returnSpec = parseCallableReturnSpec(rightParen); PbsAst.Expression elseFallback = null; - if (cursor.match(PbsTokenKind.ELSE)) { + if (allowElseFallback && cursor.match(PbsTokenKind.ELSE)) { elseFallback = exprParser.parseExpression(); } @@ -221,12 +376,40 @@ public final class PbsParser { return new PbsAst.FunctionDecl( name.lexeme(), ReadOnlyList.wrap(parameters), - returnType, + returnSpec.kind, + returnSpec.returnType, + returnSpec.resultErrorType, elseFallback, body, 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. */ @@ -234,18 +417,17 @@ public final class PbsParser { final var name = consume(PbsTokenKind.IDENTIFIER, "Expected callback name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name"); 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; - if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) { - returnType = parseTypeRef(); - } + final var returnSpec = parseCallableReturnSpec(rightParen); final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after callback declaration"); return new PbsAst.CallbackDecl( name.lexeme(), ReadOnlyList.wrap(parameters), - returnType, + returnSpec.kind, + returnSpec.returnType, + returnSpec.resultErrorType, span(declareToken.start(), semicolon.end())); } @@ -296,15 +478,6 @@ public final class PbsParser { 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 parseParametersUntilRightParen() { final var parameters = new ArrayList(); if (cursor.check(PbsTokenKind.RIGHT_PAREN)) { @@ -325,21 +498,125 @@ public final class PbsParser { 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() { - if (cursor.match(PbsTokenKind.IDENTIFIER, PbsTokenKind.SELF)) { - final var identifier = cursor.previous(); - return new PbsAst.TypeRef(identifier.lexeme(), span(identifier.start(), identifier.end())); + if (cursor.match(PbsTokenKind.OPTIONAL)) { + final var optionalToken = cursor.previous(); + 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(); - report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected type name"); + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected type surface"); if (!cursor.isAtEnd()) { cursor.advance(); } - return new PbsAst.TypeRef("", span(token.start(), token.end())); + return PbsAst.TypeRef.error(span(token.start(), token.end())); + } + + private ArrayList parseNamedTupleTypeFields() { + final var fields = new ArrayList(); + 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())); } + 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; @@ -486,6 +773,14 @@ public final class PbsParser { 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. */ @@ -500,8 +795,9 @@ public final class PbsParser { diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end())); } - @FunctionalInterface - private interface NamedDeclFactory { - PbsAst.TopDecl build(String name, Span declarationSpan); + private record ParsedReturnSpec( + PbsAst.ReturnKind kind, + PbsAst.TypeRef returnType, + PbsAst.TypeRef resultErrorType) { } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTokenCursor.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTokenCursor.java index dd143d31..7302bc92 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTokenCursor.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTokenCursor.java @@ -74,4 +74,19 @@ final class PbsTokenCursor { PbsToken previous() { 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; + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java index a364ecfd..ba712507 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java @@ -12,11 +12,11 @@ class PbsFrontendCompilerTest { @Test void shouldLowerFunctionsToIr() { final var source = """ - fn a(): int { + fn a() -> int { return 1; } - fn b(x: int): int { + fn b(x: int) -> int { return x; } """; @@ -37,8 +37,8 @@ class PbsFrontendCompilerTest { @Test void shouldReportDuplicateFunctionNames() { final var source = """ - fn a(): int { return 1; } - fn a(): int { return 2; } + fn a() -> int { return 1; } + fn a() -> int { return 2; } """; final var diagnostics = DiagnosticSink.empty(); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java index 981a8ecf..c48440a3 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java @@ -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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class PbsParserTest { @Test - void shouldParseSingleFunction() { + void shouldParseSingleFunctionWithArrowReturn() { final var source = """ - fn sum(a: int, b: int): int + fn sum(a: int, b: int) -> int { return a + b; } @@ -34,7 +35,9 @@ class PbsParserTest { final var fn = ast.functions().getFirst(); assertEquals("sum", fn.name()); assertEquals(2, fn.parameters().size()); + assertEquals(PbsAst.ReturnKind.PLAIN, fn.returnKind()); assertEquals("int", fn.returnType().name()); + assertNull(fn.resultErrorType()); assertEquals(1, fn.body().statements().size()); assertInstanceOf(PbsAst.ReturnStatement.class, fn.body().statements().getFirst()); @@ -43,13 +46,13 @@ class PbsParserTest { } @Test - void shouldPreserveImportsAndTopDeclOrdering() { + void shouldParseImportsAndTopDeclOrdering() { final var source = """ 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 callback Tick(dt: int): int; + declare callback Tick(dt: int) -> int; """; final var diagnostics = DiagnosticSink.empty(); final var fileId = new FileId(0); @@ -70,17 +73,17 @@ class PbsParserTest { } @Test - void shouldRepresentMandatoryDeclarationFamilies() { + void shouldParseDeclarationFamiliesWithShape() { final var source = """ - fn f(): int { return 1; } - declare struct S(x: int); - declare contract C { fn run(x: int): int; } - declare service Game { fn tick(x: int): int { return x; } } - declare error Err { Fail; } - declare enum Mode(Idle, Run); - declare callback TickCb(x: int): int; + fn f() -> int { return 1; } + 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 service Game { fn tick(x: int) -> int { return x; } } + declare error Err { Fail; Crash; } + declare enum Mode(Idle = 0, Run = 1); + declare callback TickCb(x: int) -> result int; 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 fileId = new FileId(0); @@ -88,24 +91,84 @@ class PbsParserTest { 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()); - assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(0)); - assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1)); - assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2)); - assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().get(3)); - assertInstanceOf(PbsAst.ErrorDecl.class, ast.topDecls().get(4)); - assertInstanceOf(PbsAst.EnumDecl.class, ast.topDecls().get(5)); - assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(6)); + + final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1)); + assertEquals(2, structDecl.fields().size()); + assertTrue(structDecl.hasBody()); + + final var contractDecl = assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2)); + 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.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 void shouldRecoverWithInvalidDeclarationNode() { final var source = """ declare ; - fn ok(): int { return 1; } + fn ok() -> int { return 1; } """; final var diagnostics = DiagnosticSink.empty(); final var fileId = new FileId(0);