From 9ddfa19bf27a5c53f52cea2abff47486f3f3b34c Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 5 Mar 2026 10:08:39 +0000 Subject: [PATCH] implements PR002 --- .../PR-001-pbs-lexer-core-syntax-alignment.md | 38 -- .../compiler/pbs/PbsFrontendCompiler.java | 8 +- .../p/studio/compiler/pbs/ast/PbsAst.java | 173 ++++++++- .../studio/compiler/pbs/parser/PbsParser.java | 339 ++++++++++++++---- .../compiler/pbs/parser/PbsParserTest.java | 82 +++++ .../compiler/source/tables/NameTable.java | 9 + .../source/tables/NameTableReader.java | 6 + .../compiler/source/tables/NameTableTest.java | 23 ++ 8 files changed, 557 insertions(+), 121 deletions(-) delete mode 100644 docs/pbs/pull-requests/PR-001-pbs-lexer-core-syntax-alignment.md create mode 100644 prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTable.java create mode 100644 prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTableReader.java create mode 100644 prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/source/tables/NameTableTest.java diff --git a/docs/pbs/pull-requests/PR-001-pbs-lexer-core-syntax-alignment.md b/docs/pbs/pull-requests/PR-001-pbs-lexer-core-syntax-alignment.md deleted file mode 100644 index 3dcff558..00000000 --- a/docs/pbs/pull-requests/PR-001-pbs-lexer-core-syntax-alignment.md +++ /dev/null @@ -1,38 +0,0 @@ -# PR-001 - PBS Lexer Core Syntax Alignment - -## Briefing -O lexer atual cobre apenas uma parte pequena da superficie definida em `3. Core Syntax Specification.md`. -Este PR fecha o contrato lexico minimo de v1 para que parser, semantica e diagnosticos trabalhem sobre um conjunto estavel de tokens. - -## Target -- Specs: - - `docs/pbs/specs/3. Core Syntax Specification.md` (secoes 4.1, 4.2, 4.4, 4.5, 10) - - `docs/pbs/specs/11. AST Specification.md` (secao 7: atribuicao estavel) -- Codigo: - - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java` - - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java` - - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/LexErrors.java` - -## Method -1. Expandir `PbsTokenKind` para todos os keywords ativos de v1 e operadores/pontuacao usados na gramatica. -2. Adicionar tokens para `COMMENT` e para operadores compostos ausentes (`->`, `+=`, `-=`, `*=`, `/=`, `%=`). -3. Suportar aliases lexicos (`and/or/not`) sem quebrar `&&/||/!`. -4. Preservar spans estaveis em todos os tokens emitidos. -5. Manter rejeicao deterministica para caracteres invalidos e strings nao terminadas. - -## Acceptance Criteria -- Lexer emite classes obrigatorias: `identifiers`, `keywords`, `literals`, `punctuation`, `operators`, `comments`, `EOF`. -- Keywords reservados nao entram como `IDENTIFIER`. -- `and/or/not` e `&&/||/!` sao tokenizados de forma consistente. -- `->` nao e decomposto em dois tokens (`-` e `>`). -- Comentarios de linha sao representados como token e nao perdem a continuidade do stream. -- Todos os tokens carregam `start/end` corretos no arquivo. - -## Tests -- `PbsLexerTest`: - - caso feliz com arquivo contendo imports, declaracoes, controle de fluxo e operadores compostos; - - caso de comentarios em multiplas linhas; - - caso de palavras reservadas vs identificadores parecidos; - - caso de string nao terminada (`E_LEX_UNTERMINATED_STRING`); - - caso de caractere invalido (`E_LEX_INVALID_CHAR`). -- Fixtures de regressao para `->`, `and/or/not` e atribuicoes compostas. 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 bf5741a4..7c0ca93c 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 @@ -7,6 +7,8 @@ import p.studio.compiler.pbs.lexer.PbsLexer; import p.studio.compiler.pbs.parser.PbsParser; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; +import p.studio.compiler.source.identifiers.NameId; +import p.studio.compiler.source.tables.NameTable; import p.studio.utilities.structures.ReadOnlyList; import java.util.ArrayList; @@ -27,9 +29,11 @@ public final class PbsFrontendCompiler { private void validateFunctionNames( final PbsAst.File ast, final DiagnosticSink diagnostics) { - final Set names = new HashSet<>(); + final var nameTable = new NameTable(); + final Set nameIds = new HashSet<>(); for (final var fn : ast.functions()) { - if (names.add(fn.name())) { + final var nameId = nameTable.register(fn.name()); + if (nameIds.add(nameId)) { continue; } diagnostics.error("E_RESOLVE_DUPLICATE_SYMBOL", 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 721d051c..bcc55a36 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 @@ -3,13 +3,57 @@ package p.studio.compiler.pbs.ast; import p.studio.compiler.source.Span; import p.studio.utilities.structures.ReadOnlyList; +import java.util.ArrayList; + public final class PbsAst { private PbsAst() { } public record File( - ReadOnlyList functions, + ReadOnlyList imports, + ReadOnlyList topDecls, Span span) { + + public ReadOnlyList functions() { + final var functions = new ArrayList(); + for (final var topDecl : topDecls) { + if (topDecl instanceof FunctionDecl functionDecl) { + functions.add(functionDecl); + } + } + return ReadOnlyList.wrap(functions); + } + } + + public record ImportDecl( + ReadOnlyList items, + ModuleRef moduleRef, + Span span) { + } + + public record ImportItem( + String name, + String alias, + Span span) { + } + + public record ModuleRef( + String project, + ReadOnlyList pathSegments, + Span span) { + } + + public sealed interface TopDecl permits FunctionDecl, + StructDecl, + ContractDecl, + ServiceDecl, + ErrorDecl, + EnumDecl, + CallbackDecl, + ConstDecl, + ImplementsDecl, + InvalidDecl { + Span span(); } public record FunctionDecl( @@ -18,7 +62,58 @@ public final class PbsAst { TypeRef returnType, Expression elseFallback, Block body, - Span span) { + Span span) implements TopDecl { + } + + public record StructDecl( + String name, + Span span) implements TopDecl { + } + + public record ContractDecl( + String name, + Span span) implements TopDecl { + } + + public record ServiceDecl( + String name, + Span span) implements TopDecl { + } + + public record ErrorDecl( + String name, + Span span) implements TopDecl { + } + + public record EnumDecl( + String name, + Span span) implements TopDecl { + } + + public record CallbackDecl( + String name, + ReadOnlyList parameters, + TypeRef returnType, + Span span) implements TopDecl { + } + + public record ConstDecl( + String name, + TypeRef explicitType, + Expression initializer, + Span span) implements TopDecl { + } + + public record ImplementsDecl( + String contractName, + String ownerName, + String binderName, + Span span) implements TopDecl { + } + + public record InvalidDecl( + String reason, + Span span) implements TopDecl { } public record Parameter( @@ -124,4 +219,78 @@ public final class PbsAst { Expression expression, Span span) implements Expression { } + + // Barrel declarations are represented explicitly for linking-visible declaration families. + public sealed interface BarrelItem permits BarrelFunctionItem, + BarrelStructItem, + BarrelContractItem, + BarrelHostItem, + BarrelErrorItem, + BarrelEnumItem, + BarrelServiceItem, + BarrelConstItem, + BarrelCallbackItem { + Span span(); + } + + public enum Visibility { + MOD, + PUB + } + + public record BarrelFunctionItem( + Visibility visibility, + String name, + ReadOnlyList parameterTypes, + TypeRef returnType, + Span span) implements BarrelItem { + } + + public record BarrelStructItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } + + public record BarrelContractItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } + + public record BarrelHostItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } + + public record BarrelErrorItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } + + public record BarrelEnumItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } + + public record BarrelServiceItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } + + public record BarrelConstItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } + + public record BarrelCallbackItem( + Visibility visibility, + String name, + Span span) implements BarrelItem { + } } 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 fd81ac2b..245272e5 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 @@ -35,13 +35,6 @@ public final class PbsParser { /** * Parses a token stream into a PBS file AST. - * - *

Example: - *

{@code
-     * fn sum(a: int, b: int): int {
-     *   return a + b;
-     * }
-     * }
*/ public static PbsAst.File parse( final ReadOnlyList tokens, @@ -52,26 +45,37 @@ public final class PbsParser { /** * Parses a full file as a sequence of imports and top-level declarations. - * - *

The current slice only stores top-level functions in the AST. */ private PbsAst.File parseFile() { - final var functions = new ArrayList(); + final var imports = new ArrayList(); + final var topDecls = new ArrayList(); while (!cursor.isAtEnd()) { if (cursor.match(PbsTokenKind.IMPORT)) { - parseAndDiscardImport(); + imports.add(parseImport(cursor.previous())); continue; } if (cursor.match(PbsTokenKind.FN)) { - functions.add(parseFunction(cursor.previous())); + topDecls.add(parseFunction(cursor.previous())); + continue; + } + + if (cursor.match(PbsTokenKind.DECLARE)) { + topDecls.add(parseDeclareTopDecl(cursor.previous())); + continue; + } + + if (cursor.match(PbsTokenKind.IMPLEMENTS)) { + topDecls.add(parseImplementsDeclaration(cursor.previous())); continue; } if (cursor.match(PbsTokenKind.MOD, PbsTokenKind.PUB)) { - report(cursor.previous(), ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE, + final var token = cursor.previous(); + report(token, ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE, "Visibility modifiers are barrel-only and cannot appear in .pbs declarations"); + topDecls.add(new PbsAst.InvalidDecl("visibility modifier in source", span(token.start(), token.end()))); synchronizeTopLevel(); continue; } @@ -80,31 +84,37 @@ public final class PbsParser { break; } - report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, - "Expected top-level declaration ('fn') or import"); + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "Expected top-level declaration ('fn', 'declare', 'implements') or import"); + topDecls.add(new PbsAst.InvalidDecl("unexpected top-level token", span(token.start(), token.end()))); synchronizeTopLevel(); } final var eof = cursor.peek(); - return new PbsAst.File(ReadOnlyList.wrap(functions), span(0, eof.end())); + return new PbsAst.File( + ReadOnlyList.wrap(imports), + ReadOnlyList.wrap(topDecls), + span(0, eof.end())); } /** - * Parses import syntax for validation and recovery, but does not store imports yet. - * - *

Supported forms: - *

{@code
-     * import @core:math;
-     * import { Vector, Matrix as Mat } from @core:math;
-     * }
+ * Parses import syntax and stores it in the AST. */ - private void parseAndDiscardImport() { + private PbsAst.ImportDecl parseImport(final PbsToken importToken) { + final var items = new ArrayList(); if (cursor.match(PbsTokenKind.LEFT_BRACE)) { while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { if (cursor.match(PbsTokenKind.IDENTIFIER)) { + final var itemName = cursor.previous(); + String alias = null; if (cursor.match(PbsTokenKind.AS)) { - consume(PbsTokenKind.IDENTIFIER, "Expected alias identifier after 'as'"); + alias = consume(PbsTokenKind.IDENTIFIER, "Expected alias identifier after 'as'").lexeme(); } + items.add(new PbsAst.ImportItem( + itemName.lexeme(), + alias, + span(itemName.start(), cursor.previous().end()))); cursor.match(PbsTokenKind.COMMA); continue; } @@ -115,54 +125,90 @@ public final class PbsParser { consume(PbsTokenKind.FROM, "Expected 'from' in named import"); } - parseModuleRef(); - consume(PbsTokenKind.SEMICOLON, "Expected ';' after import"); + final var moduleRef = parseModuleRef(); + final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after import"); + return new PbsAst.ImportDecl( + ReadOnlyList.wrap(items), + moduleRef, + span(importToken.start(), semicolon.end())); } /** * Parses a module reference such as {@code @core:math/tools}. */ - private void parseModuleRef() { - consume(PbsTokenKind.AT, "Expected '@' in module reference"); - consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference"); + private PbsAst.ModuleRef parseModuleRef() { + final var at = consume(PbsTokenKind.AT, "Expected '@' in module reference"); + final var project = consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference"); consume(PbsTokenKind.COLON, "Expected ':' in module reference"); - consume(PbsTokenKind.IDENTIFIER, "Expected module identifier"); + final var segments = new ArrayList(); + final var firstSegment = consume(PbsTokenKind.IDENTIFIER, "Expected module identifier"); + segments.add(firstSegment.lexeme()); + var end = firstSegment.end(); while (cursor.match(PbsTokenKind.SLASH)) { - consume(PbsTokenKind.IDENTIFIER, "Expected module path segment after '/'"); + final var segment = consume(PbsTokenKind.IDENTIFIER, "Expected module path segment after '/'"); + segments.add(segment.lexeme()); + end = segment.end(); } + return new PbsAst.ModuleRef(project.lexeme(), ReadOnlyList.wrap(segments), span(at.start(), end)); + } + + /** + * Parses declarations introduced by 'declare'. + */ + private PbsAst.TopDecl parseDeclareTopDecl(final PbsToken declareToken) { + if (cursor.match(PbsTokenKind.STRUCT)) { + return parseSimpleNamedDecl(declareToken, "struct", PbsAst.StructDecl::new); + } + if (cursor.match(PbsTokenKind.CONTRACT)) { + return parseSimpleNamedDecl(declareToken, "contract", PbsAst.ContractDecl::new); + } + if (cursor.match(PbsTokenKind.SERVICE)) { + return parseSimpleNamedDecl(declareToken, "service", PbsAst.ServiceDecl::new); + } + if (cursor.match(PbsTokenKind.ERROR)) { + return parseSimpleNamedDecl(declareToken, "error", PbsAst.ErrorDecl::new); + } + if (cursor.match(PbsTokenKind.ENUM)) { + return parseSimpleNamedDecl(declareToken, "enum", PbsAst.EnumDecl::new); + } + if (cursor.match(PbsTokenKind.CALLBACK)) { + return parseCallbackDeclaration(declareToken); + } + if (cursor.match(PbsTokenKind.CONST)) { + return parseConstDeclaration(declareToken); + } + if (cursor.match(PbsTokenKind.HOST)) { + final var end = consumeDeclarationTerminator(); + report(cursor.previous(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "'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); + final var end = consumeDeclarationTerminator(); + report(cursor.previous(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "'declare builtin type' is reserved and not supported in ordinary source modules"); + return new PbsAst.InvalidDecl("reserved declare builtin type", span(declareToken.start(), end)); + } + + report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "Expected declaration kind after 'declare'"); + synchronizeTopLevel(); + final var token = cursor.previous(); + return new PbsAst.InvalidDecl("invalid declare form", span(declareToken.start(), token.end())); } /** * Parses a top-level function declaration. - * - *

Example: - *

{@code
-     * fn sum(a: int, b: int): int {
-     *   return a + b;
-     * }
-     * }
*/ private PbsAst.FunctionDecl parseFunction(final PbsToken fnToken) { final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); - - final var parameters = new ArrayList(); - if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { - do { - final var parameterStart = cursor.peek(); - final var parameterName = consume(PbsTokenKind.IDENTIFIER, "Expected parameter name"); - consume(PbsTokenKind.COLON, "Expected ':' after parameter name"); - final var typeRef = parseTypeRef(); - parameters.add(new PbsAst.Parameter( - parameterName.lexeme(), - typeRef, - span(parameterStart.start(), typeRef.span().getEnd()))); - } while (cursor.match(PbsTokenKind.COMMA)); - } + final var parameters = parseParametersUntilRightParen(); consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list"); PbsAst.TypeRef returnType = null; - if (cursor.match(PbsTokenKind.COLON)) { + if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) { returnType = parseTypeRef(); } @@ -181,24 +227,123 @@ public final class PbsParser { span(fnToken.start(), body.span().getEnd())); } + /** + * Parses `declare callback` declaration. + */ + private PbsAst.CallbackDecl parseCallbackDeclaration(final PbsToken declareToken) { + 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"); + + PbsAst.TypeRef returnType = null; + if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) { + returnType = parseTypeRef(); + } + + final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after callback declaration"); + return new PbsAst.CallbackDecl( + name.lexeme(), + ReadOnlyList.wrap(parameters), + returnType, + span(declareToken.start(), semicolon.end())); + } + + /** + * Parses `declare const` declaration. + */ + private PbsAst.ConstDecl parseConstDeclaration(final PbsToken declareToken) { + final var name = consume(PbsTokenKind.IDENTIFIER, "Expected constant name"); + consume(PbsTokenKind.COLON, "Expected ':' after constant name"); + final var typeRef = parseTypeRef(); + + PbsAst.Expression initializer = null; + if (cursor.match(PbsTokenKind.EQUAL)) { + initializer = exprParser.parseExpression(); + } + + final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after constant declaration"); + return new PbsAst.ConstDecl( + name.lexeme(), + typeRef, + initializer, + span(declareToken.start(), semicolon.end())); + } + + /** + * Parses `implements Contract for Owner using binder { ... }`. + */ + private PbsAst.ImplementsDecl parseImplementsDeclaration(final PbsToken implementsToken) { + final var contractName = consume(PbsTokenKind.IDENTIFIER, "Expected contract name in 'implements'"); + consume(PbsTokenKind.FOR, "Expected 'for' in 'implements' declaration"); + final var ownerName = consume(PbsTokenKind.IDENTIFIER, "Expected owner name after 'for'"); + consume(PbsTokenKind.USING, "Expected 'using' in 'implements' declaration"); + final var binderName = consume(PbsTokenKind.IDENTIFIER, "Expected binder name after 'using'"); + + long end; + if (cursor.check(PbsTokenKind.LEFT_BRACE)) { + end = consumeBalancedBraces(cursor.advance()); + } else { + report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, + "Expected '{' to start 'implements' body"); + end = consumeDeclarationTerminator(); + } + + return new PbsAst.ImplementsDecl( + contractName.lexeme(), + ownerName.lexeme(), + binderName.lexeme(), + 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)) { + return parameters; + } + + do { + final var parameterStart = cursor.peek(); + final var parameterName = consume(PbsTokenKind.IDENTIFIER, "Expected parameter name"); + consume(PbsTokenKind.COLON, "Expected ':' after parameter name"); + final var typeRef = parseTypeRef(); + parameters.add(new PbsAst.Parameter( + parameterName.lexeme(), + typeRef, + span(parameterStart.start(), typeRef.span().getEnd()))); + } while (cursor.match(PbsTokenKind.COMMA)); + + return parameters; + } + /** * Parses a simple identifier-based type reference such as {@code int} or {@code Vector}. */ private PbsAst.TypeRef parseTypeRef() { - final var identifier = consume(PbsTokenKind.IDENTIFIER, "Expected type name"); - return new PbsAst.TypeRef(identifier.lexeme(), span(identifier.start(), identifier.end())); + if (cursor.match(PbsTokenKind.IDENTIFIER, PbsTokenKind.SELF)) { + final var identifier = cursor.previous(); + return new PbsAst.TypeRef(identifier.lexeme(), span(identifier.start(), identifier.end())); + } + + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected type name"); + if (!cursor.isAtEnd()) { + cursor.advance(); + } + return new PbsAst.TypeRef("", span(token.start(), token.end())); } /** * Parses a brace-delimited block. - * - *

Example: - *

{@code
-     * {
-     *   let x = 1;
-     *   return x;
-     * }
-     * }
*/ private PbsAst.Block parseBlock() { final var leftBrace = consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start block"); @@ -212,8 +357,6 @@ public final class PbsParser { /** * Parses one statement inside a block. - * - *

The current slice supports {@code let}, {@code return}, and expression statements. */ private PbsAst.Statement parseStatement() { if (cursor.match(PbsTokenKind.LET)) { @@ -227,14 +370,9 @@ public final class PbsParser { /** * Parses a local binding statement. - * - *

Examples: - *

{@code
-     * let x = 1;
-     * let y: int = x + 1;
-     * }
*/ private PbsAst.Statement parseLetStatement(final PbsToken letToken) { + cursor.match(PbsTokenKind.CONST); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected variable name"); PbsAst.TypeRef explicitType = null; @@ -267,8 +405,6 @@ public final class PbsParser { /** * Parses an expression statement terminated by a semicolon. - * - *

Example: {@code log(value);} */ private PbsAst.Statement parseExpressionStatement() { final var expression = exprParser.parseExpression(); @@ -276,14 +412,56 @@ public final class PbsParser { return new PbsAst.ExpressionStatement(expression, span(expression.span().getStart(), semicolon.end())); } + private long consumeDeclarationTerminator() { + var end = cursor.previous().end(); + var parenDepth = 0; + while (!cursor.isAtEnd()) { + final var token = cursor.peek(); + if (parenDepth == 0 && token.kind() == PbsTokenKind.SEMICOLON) { + return cursor.advance().end(); + } + if (parenDepth == 0 && token.kind() == PbsTokenKind.LEFT_BRACE) { + return consumeBalancedBraces(cursor.advance()); + } + + final var consumed = cursor.advance(); + end = consumed.end(); + if (consumed.kind() == PbsTokenKind.LEFT_PAREN) { + parenDepth++; + } else if (consumed.kind() == PbsTokenKind.RIGHT_PAREN) { + parenDepth = Math.max(0, parenDepth - 1); + } + } + return end; + } + + private long consumeBalancedBraces(final PbsToken leftBrace) { + var depth = 1; + var end = leftBrace.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 declaration body"); + } + return end; + } + /** * Skips tokens until a safe top-level restart point is reached. - * - *

This allows the parser to continue reporting more than one diagnostic per file. */ private void synchronizeTopLevel() { while (!cursor.isAtEnd()) { - if (cursor.check(PbsTokenKind.FN) || cursor.check(PbsTokenKind.IMPORT)) { + if (cursor.check(PbsTokenKind.FN) + || cursor.check(PbsTokenKind.IMPORT) + || cursor.check(PbsTokenKind.DECLARE) + || cursor.check(PbsTokenKind.IMPLEMENTS)) { return; } if (cursor.match(PbsTokenKind.SEMICOLON)) { @@ -295,8 +473,6 @@ public final class PbsParser { /** * Consumes a required token and reports an error if it is missing. - * - *

The parser advances on failure when possible so recovery can continue. */ private PbsToken consume(final PbsTokenKind kind, final String message) { if (cursor.check(kind)) { @@ -323,4 +499,9 @@ public final class PbsParser { private void report(final PbsToken token, final ParseErrors parseErrors, final String message) { diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end())); } + + @FunctionalInterface + private interface NamedDeclFactory { + PbsAst.TopDecl build(String name, Span declarationSpan); + } } 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 f88cbfab..981a8ecf 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 @@ -27,6 +27,8 @@ class PbsParserTest { final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics); assertTrue(diagnostics.isEmpty(), "Parser should not report diagnostics for valid function"); + assertEquals(0, ast.imports().size()); + assertEquals(1, ast.topDecls().size()); assertEquals(1, ast.functions().size()); final var fn = ast.functions().getFirst(); @@ -35,5 +37,85 @@ class PbsParserTest { assertEquals("int", fn.returnType().name()); assertEquals(1, fn.body().statements().size()); assertInstanceOf(PbsAst.ReturnStatement.class, fn.body().statements().getFirst()); + + assertEquals(fileId, fn.span().getFileId()); + assertTrue(fn.span().getEnd() > fn.span().getStart()); + } + + @Test + void shouldPreserveImportsAndTopDeclOrdering() { + final var source = """ + import { Vec2 as V2 } from @core:math; + + fn first(): int { return 1; } + declare struct Point(x: int, y: int); + declare callback Tick(dt: int): int; + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + + final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Parser should accept valid declarations in this slice"); + assertEquals(1, ast.imports().size()); + assertEquals(3, ast.topDecls().size()); + assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(0)); + assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1)); + assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(2)); + + final var importDecl = ast.imports().getFirst(); + assertEquals("core", importDecl.moduleRef().project()); + assertEquals("math", importDecl.moduleRef().pathSegments().getFirst()); + } + + @Test + void shouldRepresentMandatoryDeclarationFamilies() { + 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; + declare const LIMIT: int = 10; + implements C for S using s { fn run(x: int): int { return x; } } + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + + final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Parser should represent all mandatory declaration families"); + 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)); + assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(7)); + assertInstanceOf(PbsAst.ImplementsDecl.class, ast.topDecls().get(8)); + } + + @Test + void shouldRecoverWithInvalidDeclarationNode() { + final var source = """ + declare ; + fn ok(): int { return 1; } + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + + final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics); + + assertTrue(diagnostics.hasErrors(), "Parser should report malformed declaration"); + assertEquals(2, ast.topDecls().size()); + assertInstanceOf(PbsAst.InvalidDecl.class, ast.topDecls().get(0)); + assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(1)); } } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTable.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTable.java new file mode 100644 index 00000000..4308037a --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTable.java @@ -0,0 +1,9 @@ +package p.studio.compiler.source.tables; + +import p.studio.compiler.source.identifiers.NameId; + +public class NameTable extends InternTable implements NameTableReader { + public NameTable() { + super(NameId::new); + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTableReader.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTableReader.java new file mode 100644 index 00000000..b7d36266 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/NameTableReader.java @@ -0,0 +1,6 @@ +package p.studio.compiler.source.tables; + +import p.studio.compiler.source.identifiers.NameId; + +public interface NameTableReader extends InternTableReader { +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/source/tables/NameTableTest.java b/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/source/tables/NameTableTest.java new file mode 100644 index 00000000..6faae143 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/source/tables/NameTableTest.java @@ -0,0 +1,23 @@ +package p.studio.compiler.source.tables; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class NameTableTest { + + @Test + void shouldInternEqualNamesToSameIdentifier() { + final var table = new NameTable(); + + final var first = table.register("sum"); + final var second = table.register("sum"); + final var third = table.register("tick"); + + assertEquals(first, second); + assertEquals(2, table.size()); + assertTrue(table.containsKey("sum")); + assertEquals("tick", table.get(third)); + } +}