From f60346e1742eaf1cf6c9c9f4e32af9bf68090d02 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 5 Mar 2026 11:22:06 +0000 Subject: [PATCH] implements PR006 --- ...PR-006-pbs-barrel-and-module-visibility.md | 35 -- .../compiler/pbs/PbsFrontendCompiler.java | 7 + .../p/studio/compiler/pbs/ast/PbsAst.java | 7 + .../compiler/pbs/linking/PbsLinkErrors.java | 10 + .../linking/PbsModuleVisibilityValidator.java | 462 ++++++++++++++++++ .../compiler/pbs/parser/PbsBarrelParser.java | 308 ++++++++++++ .../services/PBSFrontendPhaseService.java | 130 ++++- .../pbs/linking/PbsModuleVisibilityTest.java | 162 ++++++ .../pbs/parser/PbsBarrelParserTest.java | 64 +++ 9 files changed, 1134 insertions(+), 51 deletions(-) delete mode 100644 docs/pbs/pull-requests/PR-006-pbs-barrel-and-module-visibility.md create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsLinkErrors.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java diff --git a/docs/pbs/pull-requests/PR-006-pbs-barrel-and-module-visibility.md b/docs/pbs/pull-requests/PR-006-pbs-barrel-and-module-visibility.md deleted file mode 100644 index 00e79a80..00000000 --- a/docs/pbs/pull-requests/PR-006-pbs-barrel-and-module-visibility.md +++ /dev/null @@ -1,35 +0,0 @@ -# PR-006 - PBS Barrel and Module Visibility - -## Briefing -A spec define `mod.barrel` como fonte unica de visibilidade, mas o frontend atual ignora `.barrel`. -Este PR implementa parser/validacao de barrel e aplica regras minimas de visibilidade/exportacao em nivel de modulo. - -## Target -- Specs: - - `docs/pbs/specs/3. Core Syntax Specification.md` (secoes 5.1, 5.2, 5.3, 6.1) - - `docs/pbs/specs/12. Diagnostics Specification.md` (fases syntax/linking) -- Codigo: - - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java` - - novo parser/modelo para `mod.barrel` em `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/`. - -## Method -1. Adicionar parser dedicado para `mod.barrel` com itens e assinaturas de funcao. -2. Detectar `missing mod.barrel` por modulo e duplicatas no barrel. -3. Validar resolucao barrel -> declaracoes top-level da AST do modulo. -4. Garantir que `pub/mod` em `.pbs` continue proibido fora dos contextos permitidos. -5. Emitir diagnosticos com atribuicao primaria no arquivo/barrel que causou a falha. - -## Acceptance Criteria -- Compilacao de modulo sem `mod.barrel` falha deterministicamente. -- Duplicatas em barrel sao detectadas por regra correta (funcao por assinatura; outros por kind+nome). -- Cada item de barrel resolve para declaracao existente de modulo. -- Itens de barrel invalidos geram erro sem quebrar analise dos demais itens. -- Importacao cross-module usa somente simbolos `pub`. - -## Tests -- `PbsBarrelParserTest` novo para shape do barrel. -- `PbsModuleVisibilityTest` novo cobrindo: - - modulo sem barrel; - - duplicatas de simbolo/assinatura; - - entry nao resolvido; - - import de simbolo nao `pub`. 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 d23348fe..0dc6a214 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 @@ -22,6 +22,13 @@ public final class PbsFrontendCompiler { final DiagnosticSink diagnostics) { final var tokens = PbsLexer.lex(source, fileId, diagnostics); final var ast = PbsParser.parse(tokens, fileId, diagnostics); + return compileParsedFile(fileId, ast, diagnostics); + } + + public IRBackendFile compileParsedFile( + final FileId fileId, + final PbsAst.File ast, + final DiagnosticSink diagnostics) { validateFunctionNames(ast, diagnostics); return new IRBackendFile(fileId, lowerFunctions(fileId, ast)); } 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 e84f4039..9c15443d 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 @@ -60,6 +60,11 @@ public final class PbsAst { Span span) { } + public record BarrelFile( + ReadOnlyList items, + Span span) { + } + public sealed interface TopDecl permits FunctionDecl, StructDecl, ContractDecl, @@ -564,7 +569,9 @@ public final class PbsAst { Visibility visibility, String name, ReadOnlyList parameterTypes, + ReturnKind returnKind, TypeRef returnType, + TypeRef resultErrorType, Span span) implements BarrelItem { } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsLinkErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsLinkErrors.java new file mode 100644 index 00000000..6a84aea4 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsLinkErrors.java @@ -0,0 +1,10 @@ +package p.studio.compiler.pbs.linking; + +public enum PbsLinkErrors { + E_LINK_MISSING_BARREL, + E_LINK_DUPLICATE_BARREL_FILE, + E_LINK_DUPLICATE_BARREL_ENTRY, + E_LINK_UNRESOLVED_BARREL_ENTRY, + E_LINK_AMBIGUOUS_BARREL_ENTRY, + E_LINK_IMPORT_SYMBOL_NOT_PUBLIC +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java new file mode 100644 index 00000000..abdfe707 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java @@ -0,0 +1,462 @@ +package p.studio.compiler.pbs.linking; + +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.diagnostics.RelatedSpan; +import p.studio.compiler.source.diagnostics.Severity; +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; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class PbsModuleVisibilityValidator { + + public void validate( + final ReadOnlyList modules, + final DiagnosticSink diagnostics) { + final var nameTable = new NameTable(); + final var exportsByModule = new HashMap(); + + for (final var module : modules) { + final var exports = validateModule(module, nameTable, diagnostics); + exportsByModule.put(toModuleRef(module.coordinates()), exports); + } + + for (final var module : modules) { + validateImports(module, exportsByModule, nameTable, diagnostics); + } + } + + private ModuleExports validateModule( + final ModuleUnit module, + final NameTable nameTable, + final DiagnosticSink diagnostics) { + final var exports = new ModuleExports(); + + if (module.sourceFiles().isEmpty()) { + return exports; + } + + if (module.barrelFiles().isEmpty()) { + diagnostics.error( + PbsLinkErrors.E_LINK_MISSING_BARREL.name(), + "Module %s is missing mod.barrel".formatted(displayModule(module.coordinates())), + module.sourceFiles().getFirst().ast().span()); + return exports; + } + + if (module.barrelFiles().size() > 1) { + final var first = module.barrelFiles().getFirst(); + for (int i = 1; i < module.barrelFiles().size(); i++) { + final var duplicate = module.barrelFiles().get(i); + diagnostics.report( + Severity.Error, + PbsLinkErrors.E_LINK_DUPLICATE_BARREL_FILE.name(), + "Module %s has multiple mod.barrel files".formatted(displayModule(module.coordinates())), + duplicate.ast().span(), + List.of(new RelatedSpan("First mod.barrel is here", first.ast().span()))); + } + } + + final var barrel = module.barrelFiles().getFirst(); + final var declarations = collectDeclarations(module, nameTable); + final Set seenNonFunctionEntries = new HashSet<>(); + final Set seenFunctionEntries = new HashSet<>(); + + for (final var item : barrel.ast().items()) { + if (item instanceof PbsAst.BarrelFunctionItem functionItem) { + final var functionKey = functionKey( + functionItem.name(), + functionItem.parameterTypes().asList(), + functionItem.returnKind(), + functionItem.returnType(), + functionItem.resultErrorType(), + nameTable); + if (!seenFunctionEntries.add(functionKey)) { + diagnostics.error( + PbsLinkErrors.E_LINK_DUPLICATE_BARREL_ENTRY.name(), + "Duplicate barrel function entry '%s' in module %s".formatted( + functionItem.name(), + displayModule(module.coordinates())), + functionItem.span()); + continue; + } + + final var matches = declarations.functionsBySignature.getOrDefault(functionKey, List.of()); + if (matches.isEmpty()) { + diagnostics.error( + PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name(), + "Barrel function entry '%s' in module %s does not resolve to a declaration".formatted( + functionItem.name(), + displayModule(module.coordinates())), + functionItem.span()); + continue; + } + if (matches.size() > 1) { + diagnostics.error( + PbsLinkErrors.E_LINK_AMBIGUOUS_BARREL_ENTRY.name(), + "Barrel function entry '%s' in module %s resolves ambiguously".formatted( + functionItem.name(), + displayModule(module.coordinates())), + functionItem.span()); + continue; + } + + if (functionItem.visibility() == PbsAst.Visibility.PUB) { + exports.publicNameIds.add(functionKey.nameId()); + } + continue; + } + + final var symbolKind = nonFunctionKind(item); + if (symbolKind == null) { + continue; + } + + final var symbolKey = new NonFunctionSymbolKey(symbolKind, nameTable.register(nonFunctionName(item))); + if (!seenNonFunctionEntries.add(symbolKey)) { + diagnostics.error( + PbsLinkErrors.E_LINK_DUPLICATE_BARREL_ENTRY.name(), + "Duplicate barrel entry '%s' in module %s".formatted( + nonFunctionName(item), + displayModule(module.coordinates())), + item.span()); + continue; + } + + final var matches = declarations.nonFunctionsByKindAndName.getOrDefault(symbolKey, List.of()); + if (matches.isEmpty()) { + diagnostics.error( + PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name(), + "Barrel entry '%s' in module %s does not resolve to a declaration".formatted( + nonFunctionName(item), + displayModule(module.coordinates())), + item.span()); + continue; + } + if (matches.size() > 1) { + diagnostics.error( + PbsLinkErrors.E_LINK_AMBIGUOUS_BARREL_ENTRY.name(), + "Barrel entry '%s' in module %s resolves ambiguously".formatted( + nonFunctionName(item), + displayModule(module.coordinates())), + item.span()); + continue; + } + + if (nonFunctionVisibility(item) == PbsAst.Visibility.PUB) { + exports.publicNameIds.add(symbolKey.nameId()); + } + } + + return exports; + } + + private void validateImports( + final ModuleUnit module, + final Map exportsByModule, + final NameTable nameTable, + final DiagnosticSink diagnostics) { + for (final var source : module.sourceFiles()) { + for (final var importDecl : source.ast().imports()) { + if (importDecl.items().isEmpty()) { + continue; + } + + final var targetModule = new ModuleRefKey( + importDecl.moduleRef().project(), + importDecl.moduleRef().pathSegments()); + final var targetExports = exportsByModule.get(targetModule); + if (targetExports == null) { + continue; + } + + for (final var importItem : importDecl.items()) { + final var importedNameId = nameTable.register(importItem.name()); + if (!targetExports.publicNameIds.contains(importedNameId)) { + diagnostics.error( + PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name(), + "Symbol '%s' is not public in module %s".formatted( + importItem.name(), + displayModule(targetModule)), + importItem.span()); + } + } + } + } + } + + private ModuleDeclarations collectDeclarations( + final ModuleUnit module, + final NameTable nameTable) { + final var declarations = new ModuleDeclarations(); + + for (final var sourceFile : module.sourceFiles()) { + for (final var topDecl : sourceFile.ast().topDecls()) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + final var key = functionKey( + functionDecl.name(), + functionDecl.parameters().stream().map(PbsAst.Parameter::typeRef).toList(), + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + nameTable); + declarations.functionsBySignature + .computeIfAbsent(key, ignored -> new ArrayList<>()) + .add(functionDecl.span()); + continue; + } + + if (topDecl instanceof PbsAst.StructDecl structDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.STRUCT, structDecl.name(), structDecl.span(), nameTable); + } else if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.CONTRACT, contractDecl.name(), contractDecl.span(), nameTable); + } else if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.ERROR, errorDecl.name(), errorDecl.span(), nameTable); + } else if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.ENUM, enumDecl.name(), enumDecl.span(), nameTable); + } else if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.SERVICE, serviceDecl.name(), serviceDecl.span(), nameTable); + } else if (topDecl instanceof PbsAst.ConstDecl constDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.CONST, constDecl.name(), constDecl.span(), nameTable); + } else if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.CALLBACK, callbackDecl.name(), callbackDecl.span(), nameTable); + } + } + } + + return declarations; + } + + private void registerNonFunctionDeclaration( + final ModuleDeclarations declarations, + final NonFunctionKind kind, + final String name, + final Span span, + final NameTable nameTable) { + final var key = new NonFunctionSymbolKey(kind, nameTable.register(name)); + declarations.nonFunctionsByKindAndName + .computeIfAbsent(key, ignored -> new ArrayList<>()) + .add(span); + } + + private FunctionSymbolKey functionKey( + final String name, + final List parameterTypes, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final NameTable nameTable) { + final var signatureSurface = signatureSurfaceKey(parameterTypes, returnKind, returnType, resultErrorType); + return new FunctionSymbolKey(nameTable.register(name), signatureSurface); + } + + private String signatureSurfaceKey( + final List parameterTypes, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + final var builder = new StringBuilder(); + builder.append('('); + for (int i = 0; i < parameterTypes.size(); i++) { + if (i > 0) { + builder.append(','); + } + builder.append(typeSurfaceKey(parameterTypes.get(i))); + } + builder.append(")->"); + + switch (returnKind) { + case INFERRED_UNIT -> builder.append("infer_unit"); + case EXPLICIT_UNIT -> builder.append("unit"); + case PLAIN -> builder.append(typeSurfaceKey(returnType)); + case RESULT -> builder.append("result<") + .append(typeSurfaceKey(resultErrorType)) + .append('>') + .append(typeSurfaceKey(returnType)); + } + + return builder.toString(); + } + + private String typeSurfaceKey(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return "unit"; + } + return switch (typeRef.kind()) { + case SIMPLE -> "simple:" + typeRef.name(); + case SELF -> "self"; + case OPTIONAL -> "optional(" + typeSurfaceKey(typeRef.inner()) + ")"; + case UNIT -> "unit"; + case GROUP -> "group(" + typeSurfaceKey(typeRef.inner()) + ")"; + case NAMED_TUPLE -> "tuple(" + typeRef.fields().stream() + .map(field -> typeSurfaceKey(field.typeRef())) + .reduce((left, right) -> left + "," + right) + .orElse("") + ")"; + case ERROR -> "error"; + }; + } + + private NonFunctionKind nonFunctionKind(final PbsAst.BarrelItem item) { + if (item instanceof PbsAst.BarrelStructItem) { + return NonFunctionKind.STRUCT; + } + if (item instanceof PbsAst.BarrelContractItem) { + return NonFunctionKind.CONTRACT; + } + if (item instanceof PbsAst.BarrelHostItem) { + return NonFunctionKind.HOST; + } + if (item instanceof PbsAst.BarrelErrorItem) { + return NonFunctionKind.ERROR; + } + if (item instanceof PbsAst.BarrelEnumItem) { + return NonFunctionKind.ENUM; + } + if (item instanceof PbsAst.BarrelServiceItem) { + return NonFunctionKind.SERVICE; + } + if (item instanceof PbsAst.BarrelConstItem) { + return NonFunctionKind.CONST; + } + if (item instanceof PbsAst.BarrelCallbackItem) { + return NonFunctionKind.CALLBACK; + } + return null; + } + + private String nonFunctionName(final PbsAst.BarrelItem item) { + if (item instanceof PbsAst.BarrelStructItem structItem) { + return structItem.name(); + } + if (item instanceof PbsAst.BarrelContractItem contractItem) { + return contractItem.name(); + } + if (item instanceof PbsAst.BarrelHostItem hostItem) { + return hostItem.name(); + } + if (item instanceof PbsAst.BarrelErrorItem errorItem) { + return errorItem.name(); + } + if (item instanceof PbsAst.BarrelEnumItem enumItem) { + return enumItem.name(); + } + if (item instanceof PbsAst.BarrelServiceItem serviceItem) { + return serviceItem.name(); + } + if (item instanceof PbsAst.BarrelConstItem constItem) { + return constItem.name(); + } + if (item instanceof PbsAst.BarrelCallbackItem callbackItem) { + return callbackItem.name(); + } + return ""; + } + + private PbsAst.Visibility nonFunctionVisibility(final PbsAst.BarrelItem item) { + if (item instanceof PbsAst.BarrelStructItem structItem) { + return structItem.visibility(); + } + if (item instanceof PbsAst.BarrelContractItem contractItem) { + return contractItem.visibility(); + } + if (item instanceof PbsAst.BarrelHostItem hostItem) { + return hostItem.visibility(); + } + if (item instanceof PbsAst.BarrelErrorItem errorItem) { + return errorItem.visibility(); + } + if (item instanceof PbsAst.BarrelEnumItem enumItem) { + return enumItem.visibility(); + } + if (item instanceof PbsAst.BarrelServiceItem serviceItem) { + return serviceItem.visibility(); + } + if (item instanceof PbsAst.BarrelConstItem constItem) { + return constItem.visibility(); + } + if (item instanceof PbsAst.BarrelCallbackItem callbackItem) { + return callbackItem.visibility(); + } + return PbsAst.Visibility.MOD; + } + + private ModuleRefKey toModuleRef(final ModuleCoordinates coordinates) { + return new ModuleRefKey(coordinates.project(), coordinates.pathSegments()); + } + + private String displayModule(final ModuleCoordinates coordinates) { + return displayModule(new ModuleRefKey(coordinates.project(), coordinates.pathSegments())); + } + + private String displayModule(final ModuleRefKey moduleRef) { + if (moduleRef.pathSegments().isEmpty()) { + return "@%s:".formatted(moduleRef.project()); + } + return "@%s:%s".formatted(moduleRef.project(), String.join("/", moduleRef.pathSegments().asList())); + } + + public record ModuleCoordinates( + String project, + ReadOnlyList pathSegments) { + } + + public record SourceFile( + FileId fileId, + PbsAst.File ast) { + } + + public record BarrelFile( + FileId fileId, + PbsAst.BarrelFile ast) { + } + + public record ModuleUnit( + ModuleCoordinates coordinates, + ReadOnlyList sourceFiles, + ReadOnlyList barrelFiles) { + } + + private record ModuleRefKey( + String project, + ReadOnlyList pathSegments) { + } + + private enum NonFunctionKind { + STRUCT, + CONTRACT, + HOST, + ERROR, + ENUM, + SERVICE, + CONST, + CALLBACK + } + + private record NonFunctionSymbolKey( + NonFunctionKind kind, + NameId nameId) { + } + + private record FunctionSymbolKey( + NameId nameId, + String signatureSurface) { + } + + private static final class ModuleDeclarations { + private final Map> nonFunctionsByKindAndName = new HashMap<>(); + private final Map> functionsBySignature = new HashMap<>(); + } + + private static final class ModuleExports { + private final Set publicNameIds = new HashSet<>(); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java new file mode 100644 index 00000000..391db369 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java @@ -0,0 +1,308 @@ +package p.studio.compiler.pbs.parser; + +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsToken; +import p.studio.compiler.pbs.lexer.PbsTokenKind; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; + +/** + * Parser for `mod.barrel` files. + */ +public final class PbsBarrelParser { + private final PbsTokenCursor cursor; + private final FileId fileId; + private final DiagnosticSink diagnostics; + + private PbsBarrelParser( + final ReadOnlyList tokens, + final FileId fileId, + final DiagnosticSink diagnostics) { + this.cursor = new PbsTokenCursor(tokens); + this.fileId = fileId; + this.diagnostics = diagnostics; + } + + public static PbsAst.BarrelFile parse( + final ReadOnlyList tokens, + final FileId fileId, + final DiagnosticSink diagnostics) { + return new PbsBarrelParser(tokens, fileId, diagnostics).parseFile(); + } + + private PbsAst.BarrelFile parseFile() { + final var items = new ArrayList(); + + while (!cursor.isAtEnd()) { + if (cursor.check(PbsTokenKind.EOF)) { + break; + } + + final var visibility = parseVisibility(); + if (visibility == null) { + synchronizeToSemicolon(); + continue; + } + + final var item = parseItem(visibility, cursor.previous()); + if (item != null) { + items.add(item); + } + } + + final var eof = cursor.peek(); + return new PbsAst.BarrelFile(ReadOnlyList.wrap(items), span(0, eof.end())); + } + + private PbsAst.Visibility parseVisibility() { + if (cursor.match(PbsTokenKind.MOD)) { + return PbsAst.Visibility.MOD; + } + if (cursor.match(PbsTokenKind.PUB)) { + return PbsAst.Visibility.PUB; + } + + report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "Expected barrel visibility ('mod' or 'pub')"); + if (!cursor.isAtEnd()) { + cursor.advance(); + } + return null; + } + + private PbsAst.BarrelItem parseItem(final PbsAst.Visibility visibility, final PbsToken visibilityToken) { + if (cursor.match(PbsTokenKind.FN)) { + return parseFunctionItem(visibility, visibilityToken); + } + if (cursor.match(PbsTokenKind.STRUCT)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.STRUCT); + } + if (cursor.match(PbsTokenKind.CONTRACT)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.CONTRACT); + } + if (cursor.match(PbsTokenKind.HOST)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.HOST); + } + if (cursor.match(PbsTokenKind.ERROR)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.ERROR); + } + if (cursor.match(PbsTokenKind.ENUM)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.ENUM); + } + if (cursor.match(PbsTokenKind.SERVICE)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.SERVICE); + } + if (cursor.match(PbsTokenKind.CONST)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.CONST); + } + if (cursor.match(PbsTokenKind.CALLBACK)) { + return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.CALLBACK); + } + + report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "Expected barrel declaration kind after visibility"); + synchronizeToSemicolon(); + return null; + } + + private PbsAst.BarrelItem parseSimpleItem( + final PbsAst.Visibility visibility, + final PbsToken visibilityToken, + final PbsTokenKind kind) { + final var name = consume(PbsTokenKind.IDENTIFIER, "Expected declaration name in barrel item"); + final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after barrel item"); + final var itemSpan = span(visibilityToken.start(), semicolon.end()); + + return switch (kind) { + case STRUCT -> new PbsAst.BarrelStructItem(visibility, name.lexeme(), itemSpan); + case CONTRACT -> new PbsAst.BarrelContractItem(visibility, name.lexeme(), itemSpan); + case HOST -> new PbsAst.BarrelHostItem(visibility, name.lexeme(), itemSpan); + case ERROR -> new PbsAst.BarrelErrorItem(visibility, name.lexeme(), itemSpan); + case ENUM -> new PbsAst.BarrelEnumItem(visibility, name.lexeme(), itemSpan); + case SERVICE -> new PbsAst.BarrelServiceItem(visibility, name.lexeme(), itemSpan); + case CONST -> new PbsAst.BarrelConstItem(visibility, name.lexeme(), itemSpan); + case CALLBACK -> new PbsAst.BarrelCallbackItem(visibility, name.lexeme(), itemSpan); + default -> null; + }; + } + + private PbsAst.BarrelFunctionItem parseFunctionItem( + final PbsAst.Visibility visibility, + final PbsToken visibilityToken) { + final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name in barrel item"); + consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); + + final var parameterTypes = new ArrayList(); + if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { + do { + consume(PbsTokenKind.IDENTIFIER, "Expected parameter name in barrel function signature"); + consume(PbsTokenKind.COLON, "Expected ':' after parameter name in barrel function signature"); + parameterTypes.add(parseTypeRef()); + } while (cursor.match(PbsTokenKind.COMMA)); + } + final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after barrel function parameters"); + final var returnSpec = parseCallableReturnSpec(rightParen); + + final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after barrel function item"); + return new PbsAst.BarrelFunctionItem( + visibility, + name.lexeme(), + ReadOnlyList.wrap(parameterTypes), + returnSpec.kind(), + returnSpec.returnType(), + returnSpec.resultErrorType(), + span(visibilityToken.start(), semicolon.end())); + } + + private ParsedReturnSpec parseCallableReturnSpec(final PbsToken anchorToken) { + if (cursor.match(PbsTokenKind.ARROW)) { + return parseReturnSpecFromMarker(); + } + + if (cursor.match(PbsTokenKind.COLON)) { + report(cursor.previous(), ParseErrors.E_PARSE_INVALID_RETURN_ANNOTATION, + "Barrel return annotations must use '->' syntax"); + return parseReturnSpecFromMarker(); + } + + final var inferredSpan = span(anchorToken.end(), anchorToken.end()); + return new ParsedReturnSpec( + PbsAst.ReturnKind.INFERRED_UNIT, + PbsAst.TypeRef.unit(inferredSpan), + null); + } + + private ParsedReturnSpec parseReturnSpecFromMarker() { + 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); + } + + private PbsAst.TypeRef parseTypeRef() { + 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 = new ArrayList(); + do { + final var label = consume(PbsTokenKind.IDENTIFIER, "Expected tuple field label"); + consume(PbsTokenKind.COLON, "Expected ':' after tuple field label"); + final var type = parseTypeRef(); + fields.add(new PbsAst.NamedTypeField(label.lexeme(), type, span(label.start(), type.span().getEnd()))); + } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN)); + + 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 surface"); + if (!cursor.isAtEnd()) { + cursor.advance(); + } + return PbsAst.TypeRef.error(span(token.start(), token.end())); + } + + private boolean isTypeStart(final PbsTokenKind kind) { + return switch (kind) { + case OPTIONAL, VOID, SELF, IDENTIFIER, LEFT_PAREN -> true; + default -> false; + }; + } + + private void synchronizeToSemicolon() { + while (!cursor.isAtEnd()) { + if (cursor.match(PbsTokenKind.SEMICOLON)) { + return; + } + if (cursor.check(PbsTokenKind.MOD) || cursor.check(PbsTokenKind.PUB)) { + return; + } + cursor.advance(); + } + } + + private PbsToken consume(final PbsTokenKind kind, final String message) { + if (cursor.check(kind)) { + return cursor.advance(); + } + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, message + ", found " + token.kind()); + if (!cursor.isAtEnd()) { + return cursor.advance(); + } + return token; + } + + private Span span(final long start, final long end) { + return new Span(fileId, start, end); + } + + private void report(final PbsToken token, final ParseErrors parseErrors, final String message) { + diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end())); + } + + 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/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 92f53939..865ffea8 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 @@ -4,13 +4,28 @@ import lombok.extern.slf4j.Slf4j; import p.studio.compiler.messages.BuildingIssueSink; import p.studio.compiler.messages.FrontendPhaseContext; import p.studio.compiler.models.IRBackend; +import p.studio.compiler.models.ProjectDescriptor; +import p.studio.compiler.models.SourceHandle; import p.studio.compiler.pbs.PbsFrontendCompiler; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.linking.PbsModuleVisibilityValidator; +import p.studio.compiler.pbs.parser.PbsBarrelParser; +import p.studio.compiler.pbs.parser.PbsParser; import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; import p.studio.utilities.logs.LogAggregator; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + @Slf4j public class PBSFrontendPhaseService implements FrontendPhaseService { private final PbsFrontendCompiler frontendCompiler = new PbsFrontendCompiler(); + private final PbsModuleVisibilityValidator moduleVisibilityValidator = new PbsModuleVisibilityValidator(); @Override public IRBackend compile( @@ -19,33 +34,116 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { final LogAggregator logs, final BuildingIssueSink issues) { final var irBackendAggregator = IRBackend.aggregator(); + final var parsedSourceFiles = new ArrayList(); + final Map modulesByCoordinates = new LinkedHashMap<>(); for (final var pId : ctx.stack.reverseTopologicalOrder) { + final var projectDescriptor = ctx.projectTable.get(pId); final var fileIds = ctx.fileTable.getFiles(pId); for (final var fId : fileIds) { final var sourceHandle = ctx.fileTable.get(fId); - switch (sourceHandle.getExtension()) { - case "pbs": { - sourceHandle.readUtf8().ifPresentOrElse( - utf8Content -> { - final var irBackendFile = frontendCompiler - .compileFile(fId, utf8Content, diagnostics); - irBackendAggregator.merge(irBackendFile); - }, - () -> issues.report(builder -> builder - .error(true) - .message("Failed to read file content: %s".formatted(sourceHandle.toString())))); - } break; - case "barrel": - break; - default: - } + sourceHandle.readUtf8().ifPresentOrElse( + utf8Content -> { + final var coordinates = resolveModuleCoordinates(projectDescriptor, sourceHandle); + final var moduleUnit = modulesByCoordinates.computeIfAbsent( + coordinates, + ignored -> new MutableModuleUnit()); + switch (sourceHandle.getExtension()) { + case "pbs" -> { + final var ast = parseSourceFile(fId, utf8Content, diagnostics); + moduleUnit.sources.add(new PbsModuleVisibilityValidator.SourceFile(fId, ast)); + parsedSourceFiles.add(new ParsedSourceFile(fId, ast)); + } + case "barrel" -> { + if ("mod.barrel".equals(sourceHandle.getFilename())) { + final var barrelAst = parseBarrelFile(fId, utf8Content, diagnostics); + moduleUnit.barrels.add(new PbsModuleVisibilityValidator.BarrelFile(fId, barrelAst)); + } + } + default -> { + } + } + }, + () -> issues.report(builder -> builder + .error(true) + .message("Failed to read file content: %s".formatted(sourceHandle.toString())))); } } + final var modules = new ArrayList(modulesByCoordinates.size()); + for (final var entry : modulesByCoordinates.entrySet()) { + final var coordinates = entry.getKey(); + final var moduleUnit = entry.getValue(); + modules.add(new PbsModuleVisibilityValidator.ModuleUnit( + coordinates, + ReadOnlyList.wrap(moduleUnit.sources), + ReadOnlyList.wrap(moduleUnit.barrels))); + } + moduleVisibilityValidator.validate(ReadOnlyList.wrap(modules), diagnostics); + + for (final var parsedSource : parsedSourceFiles) { + final var irBackendFile = frontendCompiler.compileParsedFile(parsedSource.fileId(), parsedSource.ast(), diagnostics); + irBackendAggregator.merge(irBackendFile); + } + final var irBackend = irBackendAggregator.emit(); logs.using(log).debug("PBS frontend lowered to IR BE:\n%s".formatted(irBackend)); return irBackend; } + + private PbsAst.File parseSourceFile( + final FileId fileId, + final String source, + final DiagnosticSink diagnostics) { + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + return PbsParser.parse(tokens, fileId, diagnostics); + } + + private PbsAst.BarrelFile parseBarrelFile( + final FileId fileId, + final String source, + final DiagnosticSink diagnostics) { + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + return PbsBarrelParser.parse(tokens, fileId, diagnostics); + } + + private PbsModuleVisibilityValidator.ModuleCoordinates resolveModuleCoordinates( + final ProjectDescriptor projectDescriptor, + final SourceHandle sourceHandle) { + final var sourceRelativePath = sourceRelativePath(projectDescriptor, sourceHandle); + final var modulePath = sourceRelativePath.getParent(); + final var pathSegments = new ArrayList(); + if (modulePath != null) { + for (final var segment : modulePath) { + pathSegments.add(segment.toString()); + } + } + + return new PbsModuleVisibilityValidator.ModuleCoordinates( + projectDescriptor.getName(), + ReadOnlyList.wrap(pathSegments)); + } + + private Path sourceRelativePath( + final ProjectDescriptor projectDescriptor, + final SourceHandle sourceHandle) { + final var canonPath = sourceHandle.getCanonPath(); + for (final var sourceRoot : projectDescriptor.getSourceRoots()) { + if (canonPath.startsWith(sourceRoot)) { + return sourceRoot.relativize(canonPath); + } + } + return sourceHandle.getRelativePath(); + } + + private static final class MutableModuleUnit { + private final ArrayList sources = new ArrayList<>(); + private final ArrayList barrels = new ArrayList<>(); + } + + private record ParsedSourceFile( + FileId fileId, + PbsAst.File ast) { + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java new file mode 100644 index 00000000..6a00f16b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java @@ -0,0 +1,162 @@ +package p.studio.compiler.pbs.linking; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.parser.PbsBarrelParser; +import p.studio.compiler.pbs.parser.PbsParser; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PbsModuleVisibilityTest { + + @Test + void shouldReportMissingBarrelPerModule() { + final var diagnostics = DiagnosticSink.empty(); + final var nextFileId = new AtomicInteger(0); + final var module = module("core", "math", List.of( + """ + fn sum(a: int, b: int) -> int { return a + b; } + """ + ), null, nextFileId, diagnostics); + + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(module)), diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(PbsLinkErrors.E_LINK_MISSING_BARREL.name()))); + } + + @Test + void shouldReportDuplicateBarrelEntriesByKindAndSignature() { + final var diagnostics = DiagnosticSink.empty(); + final var nextFileId = new AtomicInteger(0); + final var module = module("core", "math", List.of( + """ + fn foo(x: int) -> int { return x; } + declare struct State(v: int); + """ + ), """ + pub fn foo(a: int) -> int; + pub fn foo(b: int) -> int; + pub struct State; + pub struct State; + """, nextFileId, diagnostics); + + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(module)), diagnostics); + + final long duplicateCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsLinkErrors.E_LINK_DUPLICATE_BARREL_ENTRY.name())) + .count(); + assertEquals(2, duplicateCount); + } + + @Test + void shouldReportUnresolvedBarrelEntries() { + final var diagnostics = DiagnosticSink.empty(); + final var nextFileId = new AtomicInteger(0); + final var module = module("core", "math", List.of( + """ + fn foo() -> int { return 1; } + """ + ), """ + pub fn missing() -> int; + pub enum MissingEnum; + """, nextFileId, diagnostics); + + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(module)), diagnostics); + + final long unresolvedCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name())) + .count(); + assertEquals(2, unresolvedCount); + } + + @Test + void shouldRejectImportOfNonPublicSymbols() { + final var diagnostics = DiagnosticSink.empty(); + final var nextFileId = new AtomicInteger(0); + + final var moduleMath = module("core", "math", List.of( + """ + fn secret() -> int { return 1; } + fn open() -> int { return 2; } + """ + ), """ + mod fn secret() -> int; + pub fn open() -> int; + """, nextFileId, diagnostics); + + final var moduleApp = module("core", "app", List.of( + """ + import { secret, open } from @core:math; + fn use() -> int { return open(); } + """ + ), """ + pub fn use() -> int; + """, nextFileId, diagnostics); + + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(moduleMath, moduleApp)), diagnostics); + + final var importVisibilityDiagnostics = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name())) + .toList(); + assertEquals(1, importVisibilityDiagnostics.size()); + assertTrue(importVisibilityDiagnostics.getFirst().getMessage().contains("secret")); + } + + private PbsModuleVisibilityValidator.ModuleUnit module( + final String project, + final String modulePath, + final List sourceContents, + final String barrelContent, + final AtomicInteger nextFileId, + final DiagnosticSink diagnostics) { + final var sources = new ArrayList(); + for (final var sourceContent : sourceContents) { + final var fileId = new FileId(nextFileId.getAndIncrement()); + final var ast = parseSource(sourceContent, fileId, diagnostics); + sources.add(new PbsModuleVisibilityValidator.SourceFile(fileId, ast)); + } + + final var barrels = new ArrayList(); + if (barrelContent != null) { + final var fileId = new FileId(nextFileId.getAndIncrement()); + final var barrelAst = parseBarrel(barrelContent, fileId, diagnostics); + barrels.add(new PbsModuleVisibilityValidator.BarrelFile(fileId, barrelAst)); + } + + final var coordinates = new PbsModuleVisibilityValidator.ModuleCoordinates(project, modulePathSegments(modulePath)); + return new PbsModuleVisibilityValidator.ModuleUnit( + coordinates, + ReadOnlyList.wrap(sources), + ReadOnlyList.wrap(barrels)); + } + + private PbsAst.File parseSource( + final String source, + final FileId fileId, + final DiagnosticSink diagnostics) { + return PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + } + + private PbsAst.BarrelFile parseBarrel( + final String source, + final FileId fileId, + final DiagnosticSink diagnostics) { + return PbsBarrelParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + } + + private ReadOnlyList modulePathSegments(final String modulePath) { + if (modulePath == null || modulePath.isBlank()) { + return ReadOnlyList.empty(); + } + return ReadOnlyList.wrap(List.of(modulePath.split("/"))); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java new file mode 100644 index 00000000..506d187a --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java @@ -0,0 +1,64 @@ +package p.studio.compiler.pbs.parser; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +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.assertTrue; + +class PbsBarrelParserTest { + + @Test + void shouldParseBarrelShapeWithFunctionReturnSurfaces() { + final var source = """ + pub fn sum(a: int, b: int) -> int; + mod fn run(state: optional int) -> result (value: int, meta: float); + pub struct Vec2; + pub callback Tick; + """; + + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + final var barrel = PbsBarrelParser.parse(tokens, fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Valid barrel should parse without diagnostics"); + assertEquals(4, barrel.items().size()); + + final var sum = assertInstanceOf(PbsAst.BarrelFunctionItem.class, barrel.items().get(0)); + assertEquals("sum", sum.name()); + assertEquals(PbsAst.ReturnKind.PLAIN, sum.returnKind()); + assertEquals("int", sum.returnType().name()); + assertEquals(2, sum.parameterTypes().size()); + + final var run = assertInstanceOf(PbsAst.BarrelFunctionItem.class, barrel.items().get(1)); + assertEquals(PbsAst.ReturnKind.RESULT, run.returnKind()); + assertEquals("Err", run.resultErrorType().name()); + assertEquals(PbsAst.TypeRefKind.NAMED_TUPLE, run.returnType().kind()); + + assertInstanceOf(PbsAst.BarrelStructItem.class, barrel.items().get(2)); + assertInstanceOf(PbsAst.BarrelCallbackItem.class, barrel.items().get(3)); + } + + @Test + void shouldRecoverFromInvalidBarrelItems() { + final var source = """ + pub fn ok() -> int; + pub type Invalid; + mod struct Ready; + """; + + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var barrel = PbsBarrelParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.hasErrors(), "Invalid barrel item should report diagnostics"); + assertEquals(2, barrel.items().size()); + assertInstanceOf(PbsAst.BarrelFunctionItem.class, barrel.items().get(0)); + assertInstanceOf(PbsAst.BarrelStructItem.class, barrel.items().get(1)); + } +}