From 0770bb869aed6f9ce3611c80ae3b07a4a4d630fb Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 6 Mar 2026 12:07:38 +0000 Subject: [PATCH] implements PR024 --- ...-source-kind-and-module-origin-pipeline.md | 55 ---- ...-stdlib-environment-resolver-and-loader.md | 53 ---- ...24-pbs-parser-ast-interface-module-mode.md | 55 ---- .../p/studio/compiler/pbs/ast/PbsAst.java | 65 ++++ .../studio/compiler/pbs/parser/PbsParser.java | 291 +++++++++++++++--- .../pbs/stdlib/InterfaceModuleLoader.java | 3 +- .../services/PBSFrontendPhaseService.java | 10 +- .../compiler/pbs/parser/PbsParserTest.java | 72 +++++ .../services/PBSFrontendPhaseServiceTest.java | 58 ++++ 9 files changed, 459 insertions(+), 203 deletions(-) delete mode 100644 docs/pbs/pull-requests/PR-022-pbs-source-kind-and-module-origin-pipeline.md delete mode 100644 docs/pbs/pull-requests/PR-023-pbs-stdlib-environment-resolver-and-loader.md delete mode 100644 docs/pbs/pull-requests/PR-024-pbs-parser-ast-interface-module-mode.md diff --git a/docs/pbs/pull-requests/PR-022-pbs-source-kind-and-module-origin-pipeline.md b/docs/pbs/pull-requests/PR-022-pbs-source-kind-and-module-origin-pipeline.md deleted file mode 100644 index 1a4fe93d..00000000 --- a/docs/pbs/pull-requests/PR-022-pbs-source-kind-and-module-origin-pipeline.md +++ /dev/null @@ -1,55 +0,0 @@ -# PR-022 - PBS Source Kind and Module Origin in Frontend Pipeline - -## Briefing - -O pipeline atual trata todo `.pbs` como fonte ordinaria de projeto. -As specs exigem distincao entre source ordinario e interface-module de SDK/stdlib. - -Esta PR introduz classificacao explicita de origem de fonte no frontend para permitir regras diferentes por contexto. - -## Motivation - -Sem classificacao de origem, o parser e os validadores nao conseguem aplicar corretamente: - -- rejeicao de forms reservadas em projeto ordinario, -- admissao de forms reservadas em interface modules, -- e ownership correto de diagnosticos de resolucao/import. - -## Target - -- `FrontendPhaseContext` e dados de projeto consumidos pelo frontend PBS. -- `PBSFrontendPhaseService` e fluxo de descoberta/roteamento de fontes. - -## Scope - -- Introduzir `SourceKind`/`ModuleOrigin` no pipeline (`PROJECT`, `SDK_INTERFACE`). -- Preservar comportamento atual para fontes ordinarias. -- Nao alterar ainda parser/semantica de forms reservadas (vem nas PRs seguintes). - -## Method - -- Adicionar modelagem de origem por unidade de modulo no frontend phase. -- Classificar modulo pelo project space (`@core:*`, `@sdk:*` vs projetos ordinarios), conforme specs de resolucao. -- Propagar `SourceKind` ate o ponto de parsing/validacao para habilitar gating por contexto nas PRs seguintes. - -## Acceptance Criteria - -- Cada arquivo `.pbs` processado pelo frontend possui `SourceKind` deterministico. -- Modulos ordinarios continuam no caminho atual sem regressao. -- Modulos reservados ficam marcados para fluxo de interface-module. - -## Tests - -- Testes unitarios para classificacao de origem de modulo por project space. -- Testes de nao regressao em projeto ordinario sem stdlib. - -## Non-Goals - -- Implementar stdlib loader completo. -- Alterar AST ou parser de forms reservadas. - -## Affected Documents - -- `docs/pbs/specs/3. Core Syntax Specification.md` -- `docs/pbs/specs/5. Manifest, Stdlib, and SDK Resolution Specification.md` -- `docs/pbs/specs/8. Stdlib Environment Packaging and Loading Specification.md` diff --git a/docs/pbs/pull-requests/PR-023-pbs-stdlib-environment-resolver-and-loader.md b/docs/pbs/pull-requests/PR-023-pbs-stdlib-environment-resolver-and-loader.md deleted file mode 100644 index dc7c15d2..00000000 --- a/docs/pbs/pull-requests/PR-023-pbs-stdlib-environment-resolver-and-loader.md +++ /dev/null @@ -1,53 +0,0 @@ -# PR-023 - PBS Stdlib Environment Resolver and Interface Loader - -## Briefing - -Para suportar interface modules reais, o frontend precisa resolver project spaces reservados (`@core:*`, `@sdk:*`) fora da trilha de dependencias ordinarias. - -Esta PR introduz o loader/resolver de stdlib environment com fronteiras explicitas. - -## Motivation - -As specs definem que stdlib e import ordinario possuem resolucoes distintas e sem fallback cruzado. -Sem essa separacao, interface modules nao podem ser montados de forma normativa. - -## Target - -- Abstracoes de stdlib no frontend/compiler pipeline. -- Fluxo de import para project spaces reservados. - -## Scope - -- Introduzir `StdlibEnvironment`, `StdlibModuleResolver`, `StdlibModuleSource`, `InterfaceModuleLoader`. -- Integrar roteamento no `PBSFrontendPhaseService` para imports reservados. -- Manter resolucao ordinaria inalterada para `@project:*` nao reservado. - -## Method - -- Implementar resolucao por linha de stdlib selecionada no root manifest. -- Carregar modulo reservado como fonte PBS real (nao tabela hardcoded). -- Emitir erro deterministico quando modulo reservado nao for encontrado/carregado. - -## Acceptance Criteria - -- Imports reservados nao passam por resolver ordinario. -- Imports ordinarios nao passam por stdlib resolver. -- Falha de resolucao reservada gera diagnostico deterministico e atribuivel. - -## Tests - -- Fixture: import reservado resolvido via stdlib resolver. -- Fixture: modulo reservado inexistente com erro deterministico. -- Fixture: tentativa de resolver reservado via trilha ordinaria rejeitada. - -## Non-Goals - -- Validacao semantica de forms reservadas dentro do modulo carregado. -- Extracao de metadata de builtins/host para lowering. - -## Affected Documents - -- `docs/pbs/specs/5. Manifest, Stdlib, and SDK Resolution Specification.md` -- `docs/pbs/specs/8. Stdlib Environment Packaging and Loading Specification.md` -- `docs/pbs/specs/12. Diagnostics Specification.md` - diff --git a/docs/pbs/pull-requests/PR-024-pbs-parser-ast-interface-module-mode.md b/docs/pbs/pull-requests/PR-024-pbs-parser-ast-interface-module-mode.md deleted file mode 100644 index 26740bd2..00000000 --- a/docs/pbs/pull-requests/PR-024-pbs-parser-ast-interface-module-mode.md +++ /dev/null @@ -1,55 +0,0 @@ -# PR-024 - PBS Parser and AST Support for Interface-Module Mode - -## Briefing - -Hoje o parser rejeita atributos no topo e `declare host`/`declare builtin type` sempre. -Em interface-module mode, essas forms reservadas devem ser parseadas e representadas no AST. - -## Motivation - -Sem AST/parser para forms reservadas: - -- nao ha como validar semantica de SDK/stdlib, -- nao ha como extrair metadata (`Host`, `BuiltinType`, `Slot`, `IntrinsicCall`), -- e nao ha base para linking/lowering dessas surfaces. - -## Target - -- `PbsAst` (novos nodes de declaracao e metadata de atributo). -- `PbsParser` com `SourceKind`/mode-aware parsing. - -## Scope - -- Adicionar no AST: `HostDecl`, `BuiltinTypeDecl`, shapes de atributo e anexacao por posicao valida. -- Em modo `PROJECT`: manter rejeicao de forms reservadas e atributos fora de regras. -- Em modo `SDK_INTERFACE`: parsear forms reservadas e attributes permitidas. - -## Method - -- Tornar parser mode-aware (`ORDINARY`, `INTERFACE_MODULE`). -- Substituir rejeicao hardcoded por gating por mode. -- Preservar spans, recovery e diagnosticos deterministicos. - -## Acceptance Criteria - -- Modulo ordinario continua rejeitando `declare host`/`declare builtin type` e usos invalidos de atributos. -- Interface module aceita as forms reservadas sintaticamente previstas. -- AST exposto inclui metadata necessaria para fases seguintes. - -## Tests - -- Parser tests para ordinario vs interface-module com mesmos tokens. -- Tests de recovery e spans nas novas declaracoes. -- Tests negativos para formas reservadas malformadas. - -## Non-Goals - -- Julgamento semantico completo das attributes reservadas. -- Resolucao de imports reservados (ja coberta em PR anterior). - -## Affected Documents - -- `docs/pbs/specs/3. Core Syntax Specification.md` -- `docs/pbs/specs/11. AST Specification.md` -- `docs/pbs/specs/12. Diagnostics Specification.md` - 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 150e99a7..482b047a 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 @@ -65,9 +65,50 @@ public final class PbsAst { Span span) { } + public record Attribute( + String name, + ReadOnlyList arguments, + Span span) { + } + + public record AttributeArgument( + String name, + AttributeValue value, + Span span) { + } + + public sealed interface AttributeValue permits AttributeStringValue, + AttributeIntValue, + AttributeBoolValue, + AttributeErrorValue { + Span span(); + } + + public record AttributeStringValue( + String value, + Span span) implements AttributeValue { + } + + public record AttributeIntValue( + long value, + Span span) implements AttributeValue { + } + + public record AttributeBoolValue( + boolean value, + Span span) implements AttributeValue { + } + + public record AttributeErrorValue( + String value, + Span span) implements AttributeValue { + } + public sealed interface TopDecl permits FunctionDecl, StructDecl, ContractDecl, + HostDecl, + BuiltinTypeDecl, ServiceDecl, ErrorDecl, EnumDecl, @@ -103,6 +144,20 @@ public final class PbsAst { Span span) implements TopDecl { } + public record HostDecl( + String name, + ReadOnlyList signatures, + Span span) implements TopDecl { + } + + public record BuiltinTypeDecl( + String name, + ReadOnlyList fields, + ReadOnlyList signatures, + ReadOnlyList attributes, + Span span) implements TopDecl { + } + public record ServiceDecl( String name, ReadOnlyList methods, @@ -134,6 +189,7 @@ public final class PbsAst { String name, TypeRef explicitType, Expression initializer, + ReadOnlyList attributes, Span span) implements TopDecl { } @@ -212,12 +268,21 @@ public final class PbsAst { Span span) { } + public record BuiltinFieldDecl( + String name, + TypeRef typeRef, + boolean isPublic, + ReadOnlyList attributes, + Span span) { + } + public record FunctionSignature( String name, ReadOnlyList parameters, ReturnKind returnKind, TypeRef returnType, TypeRef resultErrorType, + ReadOnlyList attributes, Span span) { } 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 9e458608..b74bfed6 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 @@ -23,20 +23,28 @@ import java.util.List; public final class PbsParser { private static final int MAX_NAMED_TUPLE_ARITY = 6; + public enum ParseMode { + ORDINARY, + INTERFACE_MODULE + } + private final PbsTokenCursor cursor; private final PbsExprParser exprParser; private final FileId fileId; private final DiagnosticSink diagnostics; + private final ParseMode parseMode; private int loopDepth; private PbsParser( final ReadOnlyList tokens, final FileId fileId, - final DiagnosticSink diagnostics) { + final DiagnosticSink diagnostics, + final ParseMode parseMode) { this.cursor = new PbsTokenCursor(tokens); this.exprParser = new PbsExprParser(cursor, fileId, diagnostics, this::parseExpressionSurfaceBlock); this.fileId = fileId; this.diagnostics = diagnostics; + this.parseMode = parseMode; } /** @@ -46,7 +54,15 @@ public final class PbsParser { final ReadOnlyList tokens, final FileId fileId, final DiagnosticSink diagnostics) { - return new PbsParser(tokens, fileId, diagnostics).parseFile(); + return parse(tokens, fileId, diagnostics, ParseMode.ORDINARY); + } + + public static PbsAst.File parse( + final ReadOnlyList tokens, + final FileId fileId, + final DiagnosticSink diagnostics, + final ParseMode parseMode) { + return new PbsParser(tokens, fileId, diagnostics, parseMode).parseFile(); } /** @@ -57,31 +73,42 @@ public final class PbsParser { final var topDecls = new ArrayList(); while (!cursor.isAtEnd()) { + var pendingAttributes = ReadOnlyList.empty(); if (cursor.check(PbsTokenKind.LEFT_BRACKET)) { - parseRejectedAttributeList(); + pendingAttributes = parseAttributeList(); + if (isOrdinaryMode()) { + reportAttributesNotAllowed( + pendingAttributes, + "Attributes are not allowed in ordinary .pbs source modules"); + pendingAttributes = ReadOnlyList.empty(); + } } if (cursor.match(PbsTokenKind.IMPORT)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "import declarations"); imports.add(parseImport(cursor.previous())); continue; } if (cursor.match(PbsTokenKind.FN)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "top-level functions"); topDecls.add(parseFunction(cursor.previous())); continue; } if (cursor.match(PbsTokenKind.DECLARE)) { - topDecls.add(parseDeclareTopDecl(cursor.previous())); + topDecls.add(parseDeclareTopDecl(cursor.previous(), pendingAttributes)); continue; } if (cursor.match(PbsTokenKind.IMPLEMENTS)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "implements declarations"); topDecls.add(parseImplementsDeclaration(cursor.previous())); continue; } if (cursor.match(PbsTokenKind.MOD, PbsTokenKind.PUB)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "barrel-only visibility modifiers"); final var token = cursor.previous(); report(token, ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE, "Visibility modifiers are barrel-only and cannot appear in .pbs declarations"); @@ -91,9 +118,15 @@ public final class PbsParser { } if (cursor.check(PbsTokenKind.EOF)) { + if (!pendingAttributes.isEmpty()) { + reportAttributesNotAllowed(pendingAttributes, "Attributes must precede a valid declaration"); + } break; } + if (!pendingAttributes.isEmpty()) { + reportAttributesNotAllowed(pendingAttributes, "Attributes must precede a valid declaration"); + } final var token = cursor.peek(); report(token, ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Expected top-level declaration ('fn', 'declare', 'implements') or import"); @@ -165,40 +198,59 @@ public final class PbsParser { /** * Parses declarations introduced by 'declare'. */ - private PbsAst.TopDecl parseDeclareTopDecl(final PbsToken declareToken) { + private PbsAst.TopDecl parseDeclareTopDecl( + final PbsToken declareToken, + final ReadOnlyList pendingAttributes) { if (cursor.match(PbsTokenKind.STRUCT)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "struct declarations"); return parseStructDeclaration(declareToken); } if (cursor.match(PbsTokenKind.CONTRACT)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "contract declarations"); return parseContractDeclaration(declareToken); } if (cursor.match(PbsTokenKind.SERVICE)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "service declarations"); return parseServiceDeclaration(declareToken); } if (cursor.match(PbsTokenKind.ERROR)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "error declarations"); return parseErrorDeclaration(declareToken); } if (cursor.match(PbsTokenKind.ENUM)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "enum declarations"); return parseEnumDeclaration(declareToken); } if (cursor.match(PbsTokenKind.CALLBACK)) { + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "callback declarations"); return parseCallbackDeclaration(declareToken); } if (cursor.match(PbsTokenKind.CONST)) { - return parseConstDeclaration(declareToken); + return parseConstDeclaration(declareToken, pendingAttributes); } if (cursor.match(PbsTokenKind.HOST)) { - final var end = consumeDeclarationTerminator(); - 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)); + rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "host declarations"); + if (!isInterfaceMode()) { + final var end = consumeDeclarationTerminator(); + 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)); + } + return parseHostDeclaration(declareToken); } if (cursor.match(PbsTokenKind.BUILTIN)) { consume(PbsTokenKind.TYPE, "Expected 'type' in 'declare builtin type' declaration"); - final var end = consumeDeclarationTerminator(); - 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)); + if (!isInterfaceMode()) { + final var end = consumeDeclarationTerminator(); + 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)); + } + return parseBuiltinTypeDeclaration(declareToken, pendingAttributes); + } + + if (!pendingAttributes.isEmpty()) { + reportAttributesNotAllowed(pendingAttributes, "Attributes are not valid for this declaration form"); } report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, @@ -208,6 +260,133 @@ public final class PbsParser { return new PbsAst.InvalidDecl("invalid declare form", span(declareToken.start(), token.end())); } + private PbsAst.HostDecl parseHostDeclaration(final PbsToken declareToken) { + final var name = consume(PbsTokenKind.IDENTIFIER, "Expected host declaration name"); + consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start host declaration body"); + final var signatures = new ArrayList(); + while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { + var attributes = ReadOnlyList.empty(); + if (cursor.check(PbsTokenKind.LEFT_BRACKET)) { + attributes = parseAttributeList(); + } + if (!cursor.match(PbsTokenKind.FN)) { + report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, + "Host declaration body accepts only function signatures"); + cursor.advance(); + continue; + } + signatures.add(parseFunctionSignature(cursor.previous(), true, attributes)); + } + final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end host declaration body"); + return new PbsAst.HostDecl( + name.lexeme(), + ReadOnlyList.wrap(signatures), + span(declareToken.start(), rightBrace.end())); + } + + private PbsAst.BuiltinTypeDecl parseBuiltinTypeDeclaration( + final PbsToken declareToken, + final ReadOnlyList declarationAttributes) { + final var name = consume(PbsTokenKind.IDENTIFIER, "Expected builtin type name"); + consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in builtin type declaration"); + final var fields = parseBuiltinTypeFields(); + consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after builtin type fields"); + + consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start builtin type body"); + final var signatures = new ArrayList(); + while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { + var attributes = ReadOnlyList.empty(); + if (cursor.check(PbsTokenKind.LEFT_BRACKET)) { + attributes = parseAttributeList(); + } + if (!cursor.match(PbsTokenKind.FN)) { + report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, + "Builtin type body accepts only function signatures"); + cursor.advance(); + continue; + } + signatures.add(parseFunctionSignature(cursor.previous(), true, attributes)); + } + final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end builtin type body"); + return new PbsAst.BuiltinTypeDecl( + name.lexeme(), + ReadOnlyList.wrap(fields), + ReadOnlyList.wrap(signatures), + declarationAttributes, + span(declareToken.start(), rightBrace.end())); + } + + private ArrayList parseBuiltinTypeFields() { + final var fields = new ArrayList(); + if (cursor.check(PbsTokenKind.RIGHT_PAREN)) { + return fields; + } + + do { + final var start = cursor.peek(); + var attributes = ReadOnlyList.empty(); + if (cursor.check(PbsTokenKind.LEFT_BRACKET)) { + attributes = parseAttributeList(); + } + + var isPublic = false; + if (cursor.match(PbsTokenKind.PUB)) { + isPublic = true; + if (cursor.match(PbsTokenKind.MUT)) { + final var mutToken = cursor.previous(); + report(mutToken, ParseErrors.E_PARSE_INVALID_DECL_SHAPE, + "'mut' is not allowed in builtin type fields"); + } + } else if (cursor.match(PbsTokenKind.MUT)) { + final var mutToken = cursor.previous(); + report(mutToken, ParseErrors.E_PARSE_INVALID_DECL_SHAPE, + "'mut' is not allowed in builtin type fields"); + } + + final var fieldName = consume(PbsTokenKind.IDENTIFIER, "Expected builtin field name"); + consume(PbsTokenKind.COLON, "Expected ':' after builtin field name"); + final var typeRef = parseTypeRef(); + fields.add(new PbsAst.BuiltinFieldDecl( + fieldName.lexeme(), + typeRef, + isPublic, + attributes, + span(start.start(), typeRef.span().getEnd()))); + } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN)); + + return fields; + } + + private void rejectAttributesBeforeUnsupportedTopDecl( + final ReadOnlyList attributes, + final String targetSurface) { + if (attributes.isEmpty()) { + return; + } + reportAttributesNotAllowed( + attributes, + "Attributes are not allowed before " + targetSurface); + } + + private void reportAttributesNotAllowed( + final ReadOnlyList attributes, + final String message) { + for (final var attribute : attributes) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name(), + message, + attribute.span()); + } + } + + private boolean isInterfaceMode() { + return parseMode == ParseMode.INTERFACE_MODULE; + } + + private boolean isOrdinaryMode() { + return parseMode == ParseMode.ORDINARY; + } + private PbsAst.StructDecl parseStructDeclaration(final PbsToken declareToken) { final var name = consume(PbsTokenKind.IDENTIFIER, "Expected struct name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in struct declaration"); @@ -278,21 +457,20 @@ public final class PbsParser { return fields; } - private void parseRejectedAttributeList() { + private ReadOnlyList parseAttributeList() { + final var attributes = new ArrayList(); while (cursor.check(PbsTokenKind.LEFT_BRACKET)) { - final var attributeSpan = parseAttribute(); - p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, - ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name(), - "Attributes are not allowed in ordinary .pbs source modules", - attributeSpan); + attributes.add(parseAttribute()); } + return ReadOnlyList.wrap(attributes); } - private Span parseAttribute() { + private PbsAst.Attribute parseAttribute() { final var leftBracket = consume(PbsTokenKind.LEFT_BRACKET, "Expected '[' to start attribute"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected attribute identifier"); + final var arguments = new ArrayList(); if (cursor.match(PbsTokenKind.LEFT_PAREN)) { - parseAttributeArguments(); + parseAttributeArguments(arguments); } long end = Math.max(leftBracket.end(), name.end()); @@ -302,13 +480,16 @@ public final class PbsParser { report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected ']' to close attribute"); end = recoverUntilAttributeCloseOrTopLevel(end); } - return span(leftBracket.start(), end); + return new PbsAst.Attribute( + name.lexeme(), + ReadOnlyList.wrap(arguments), + span(leftBracket.start(), end)); } - private void parseAttributeArguments() { + private void parseAttributeArguments(final ArrayList arguments) { if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { do { - parseAttributeArgument(); + arguments.add(parseAttributeArgument()); } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN)); } @@ -319,24 +500,49 @@ public final class PbsParser { recoverUntilAttributeArgumentClose(); } - private void parseAttributeArgument() { - consume(PbsTokenKind.IDENTIFIER, "Expected attribute argument name"); + private PbsAst.AttributeArgument parseAttributeArgument() { + final var argumentStart = cursor.peek(); + final var name = consume(PbsTokenKind.IDENTIFIER, "Expected attribute argument name"); consume(PbsTokenKind.EQUAL, "Expected '=' in attribute argument"); - parseAttributeValue(); + final var value = parseAttributeValue(); + return new PbsAst.AttributeArgument( + name.lexeme(), + value, + span(argumentStart.start(), value.span().getEnd())); } - private void parseAttributeValue() { - if (cursor.match(PbsTokenKind.STRING_LITERAL, PbsTokenKind.INT_LITERAL, PbsTokenKind.TRUE, PbsTokenKind.FALSE)) { - return; + private PbsAst.AttributeValue parseAttributeValue() { + if (cursor.match(PbsTokenKind.STRING_LITERAL)) { + final var token = cursor.previous(); + return new PbsAst.AttributeStringValue(token.lexeme(), span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.INT_LITERAL)) { + final var token = cursor.previous(); + final var parsedLong = parseLongOrNull(token.lexeme()); + if (parsedLong != null) { + return new PbsAst.AttributeIntValue(parsedLong, span(token.start(), token.end())); + } + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Invalid integer literal in attribute argument"); + return new PbsAst.AttributeErrorValue(token.lexeme(), span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.TRUE)) { + final var token = cursor.previous(); + return new PbsAst.AttributeBoolValue(true, span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.FALSE)) { + final var token = cursor.previous(); + return new PbsAst.AttributeBoolValue(false, span(token.start(), token.end())); } - report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected attribute value (string, int, or bool)"); + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected attribute value (string, int, or bool)"); if (!cursor.isAtEnd() && !cursor.check(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN) && !cursor.check(PbsTokenKind.RIGHT_BRACKET)) { cursor.advance(); } + return new PbsAst.AttributeErrorValue("", span(token.start(), token.end())); } private long recoverUntilAttributeCloseOrTopLevel(final long fallbackEnd) { @@ -403,13 +609,21 @@ public final class PbsParser { consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start contract body"); final var signatures = new ArrayList(); while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { + var attributes = ReadOnlyList.empty(); + if (cursor.check(PbsTokenKind.LEFT_BRACKET)) { + attributes = parseAttributeList(); + if (isOrdinaryMode()) { + reportAttributesNotAllowed(attributes, "Attributes are not allowed in ordinary .pbs source modules"); + attributes = ReadOnlyList.empty(); + } + } 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)); + signatures.add(parseFunctionSignature(cursor.previous(), true, attributes)); } 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())); @@ -530,7 +744,10 @@ public final class PbsParser { span(fnToken.start(), body.span().getEnd())); } - private PbsAst.FunctionSignature parseFunctionSignature(final PbsToken fnToken, final boolean requireSemicolon) { + private PbsAst.FunctionSignature parseFunctionSignature( + final PbsToken fnToken, + final boolean requireSemicolon, + final ReadOnlyList attributes) { final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); final var parameters = parseParametersUntilRightParen(); @@ -549,6 +766,7 @@ public final class PbsParser { returnSpec.kind, returnSpec.returnType, returnSpec.resultErrorType, + attributes, span(fnToken.start(), end)); } @@ -576,7 +794,9 @@ public final class PbsParser { /** * Parses `declare const` declaration. */ - private PbsAst.ConstDecl parseConstDeclaration(final PbsToken declareToken) { + private PbsAst.ConstDecl parseConstDeclaration( + final PbsToken declareToken, + final ReadOnlyList attributes) { final var name = consume(PbsTokenKind.IDENTIFIER, "Expected constant name"); consume(PbsTokenKind.COLON, "Expected ':' after constant name"); final var typeRef = parseTypeRef(); @@ -591,6 +811,7 @@ public final class PbsParser { name.lexeme(), typeRef, initializer, + attributes, span(declareToken.start(), semicolon.end())); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/InterfaceModuleLoader.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/InterfaceModuleLoader.java index 40fe6b0a..298d07d6 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/InterfaceModuleLoader.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/stdlib/InterfaceModuleLoader.java @@ -49,7 +49,7 @@ public final class InterfaceModuleLoader { final FileId fileId, final DiagnosticSink diagnostics) { final var tokens = PbsLexer.lex(source, fileId, diagnostics); - return PbsParser.parse(tokens, fileId, diagnostics); + return PbsParser.parse(tokens, fileId, diagnostics, PbsParser.ParseMode.INTERFACE_MODULE); } private PbsAst.BarrelFile parseBarrel( @@ -60,4 +60,3 @@ public final class InterfaceModuleLoader { return PbsBarrelParser.parse(tokens, fileId, diagnostics); } } - diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index ea0d2e1f..5baca79c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -82,7 +82,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { case "pbs" -> { moduleKeyByFile.put(fId, moduleKey); final var parseErrorBaseline = diagnostics.errorCount(); - final var ast = parseSourceFile(fId, utf8Content, diagnostics); + final var ast = parseSourceFile(fId, utf8Content, diagnostics, projectSourceKind); moduleUnit.sources.add(new PbsModuleVisibilityValidator.SourceFile(fId, ast)); parsedSourceFiles.add(new ParsedSourceFile(fId, ast, moduleKey, projectSourceKind)); if (diagnostics.errorCount() > parseErrorBaseline) { @@ -210,9 +210,13 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { private PbsAst.File parseSourceFile( final FileId fileId, final String source, - final DiagnosticSink diagnostics) { + final DiagnosticSink diagnostics, + final SourceKind sourceKind) { final var tokens = PbsLexer.lex(source, fileId, diagnostics); - return PbsParser.parse(tokens, fileId, diagnostics); + final var parseMode = sourceKind == SourceKind.SDK_INTERFACE + ? PbsParser.ParseMode.INTERFACE_MODULE + : PbsParser.ParseMode.ORDINARY; + return PbsParser.parse(tokens, fileId, diagnostics, parseMode); } private PbsAst.BarrelFile parseBarrelFile( 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 7e4d989c..b600ec93 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 @@ -216,6 +216,78 @@ class PbsParserTest { assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(2)); } + @Test + void shouldParseReservedDeclarationsInInterfaceModuleMode() { + final var source = """ + [BuiltinType(name = "Color", version = 1)] + declare builtin type Color( + [Slot(index = 0)] pub r: int, + [Slot(index = 1)] pub g: int, + [Slot(index = 2)] pub b: int, + ) { + [IntrinsicCall(name = "color.pack", version = 1)] + fn pack() -> int; + } + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw(x: int, y: int, color: Color) -> void; + } + [BuiltinConst(target = "Color", name = "white", version = 1)] + declare const WHITE: Color; + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + + final PbsAst.File ast = PbsParser.parse( + PbsLexer.lex(source, fileId, diagnostics), + fileId, + diagnostics, + PbsParser.ParseMode.INTERFACE_MODULE); + + assertTrue(diagnostics.isEmpty(), "Parser should accept reserved interface declarations in interface-module mode"); + assertEquals(3, ast.topDecls().size()); + + final var builtinType = assertInstanceOf(PbsAst.BuiltinTypeDecl.class, ast.topDecls().get(0)); + assertEquals(1, builtinType.attributes().size()); + assertEquals(3, builtinType.fields().size()); + assertEquals(1, builtinType.signatures().size()); + assertEquals(1, builtinType.fields().getFirst().attributes().size()); + assertEquals(1, builtinType.signatures().getFirst().attributes().size()); + + final var hostDecl = assertInstanceOf(PbsAst.HostDecl.class, ast.topDecls().get(1)); + assertEquals(1, hostDecl.signatures().size()); + assertEquals(1, hostDecl.signatures().getFirst().attributes().size()); + + final var constDecl = assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(2)); + assertEquals(1, constDecl.attributes().size()); + } + + @Test + void shouldRecoverFromMalformedReservedDeclarationsInInterfaceModuleMode() { + final var source = """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw(x: int, y: int) -> void; + unexpected_token + } + 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, + PbsParser.ParseMode.INTERFACE_MODULE); + + assertTrue(diagnostics.hasErrors(), "Malformed reserved forms must still emit deterministic parse diagnostics"); + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_INVALID_DECL_SHAPE.name()))); + assertEquals(2, ast.topDecls().size()); + assertInstanceOf(PbsAst.HostDecl.class, ast.topDecls().get(0)); + assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(1)); + } + @Test void shouldRejectFunctionElseFallbackSurface() { final var source = """ diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java index ec8b2b23..f028f6c8 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java @@ -223,6 +223,64 @@ class PBSFrontendPhaseServiceTest { assertEquals(0, irBackend.getFunctions().size()); } + @Test + void shouldParseReservedDeclarationsWhenSourceKindIsSdkInterface() throws IOException { + final var projectRoot = tempDir.resolve("project-sdk-interface-reserved"); + final var sourceRoot = projectRoot.resolve("src"); + final var modulePath = sourceRoot.resolve("gfx"); + Files.createDirectories(modulePath); + + final var sourceFile = modulePath.resolve("source.pbs"); + final var modBarrel = modulePath.resolve("mod.barrel"); + Files.writeString(sourceFile, """ + [BuiltinType(name = "Color", version = 1)] + declare builtin type Color( + [Slot(index = 0)] pub r: int, + [Slot(index = 1)] pub g: int, + [Slot(index = 2)] pub b: int + ) { + [IntrinsicCall(name = "color.pack", version = 1)] + fn pack() -> int; + } + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw(x: int, y: int, color: Color) -> void; + } + """); + Files.writeString(modBarrel, ""); + + final var projectTable = new ProjectTable(); + final var fileTable = new FileTable(1); + final var projectId = projectTable.register(ProjectDescriptor.builder() + .rootPath(projectRoot) + .name("gfx-sdk") + .version("1.0.0") + .sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot))) + .sourceKind(SourceKind.SDK_INTERFACE) + .build()); + + registerFile(projectId, projectRoot, sourceFile, fileTable); + registerFile(projectId, projectRoot, modBarrel, fileTable); + + final var ctx = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId)))); + final var diagnostics = DiagnosticSink.empty(); + + final var irBackend = new PBSFrontendPhaseService().compile( + ctx, + diagnostics, + LogAggregator.empty(), + BuildingIssueSink.empty()); + + assertTrue(diagnostics.stream().noneMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name()))); + assertTrue(diagnostics.stream().noneMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_MISSING_BARREL.name()))); + assertEquals(0, irBackend.getFunctions().size()); + } + @Test void shouldResolveReservedImportsThroughStdlibResolverForSelectedMajor() throws IOException { final var projectRoot = tempDir.resolve("project-stdlib");