implements PR006

This commit is contained in:
bQUARKz 2026-03-05 11:22:06 +00:00
parent 931c5d96a3
commit f60346e174
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
9 changed files with 1134 additions and 51 deletions

View File

@ -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`.

View File

@ -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));
}

View File

@ -60,6 +60,11 @@ public final class PbsAst {
Span span) {
}
public record BarrelFile(
ReadOnlyList<BarrelItem> items,
Span span) {
}
public sealed interface TopDecl permits FunctionDecl,
StructDecl,
ContractDecl,
@ -564,7 +569,9 @@ public final class PbsAst {
Visibility visibility,
String name,
ReadOnlyList<TypeRef> parameterTypes,
ReturnKind returnKind,
TypeRef returnType,
TypeRef resultErrorType,
Span span) implements BarrelItem {
}

View File

@ -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
}

View File

@ -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<ModuleUnit> modules,
final DiagnosticSink diagnostics) {
final var nameTable = new NameTable();
final var exportsByModule = new HashMap<ModuleRefKey, ModuleExports>();
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<NonFunctionSymbolKey> seenNonFunctionEntries = new HashSet<>();
final Set<FunctionSymbolKey> 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<ModuleRefKey, ModuleExports> 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<PbsAst.TypeRef> 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<PbsAst.TypeRef> 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 "<unknown>";
}
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:<root>".formatted(moduleRef.project());
}
return "@%s:%s".formatted(moduleRef.project(), String.join("/", moduleRef.pathSegments().asList()));
}
public record ModuleCoordinates(
String project,
ReadOnlyList<String> pathSegments) {
}
public record SourceFile(
FileId fileId,
PbsAst.File ast) {
}
public record BarrelFile(
FileId fileId,
PbsAst.BarrelFile ast) {
}
public record ModuleUnit(
ModuleCoordinates coordinates,
ReadOnlyList<SourceFile> sourceFiles,
ReadOnlyList<BarrelFile> barrelFiles) {
}
private record ModuleRefKey(
String project,
ReadOnlyList<String> 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<NonFunctionSymbolKey, List<Span>> nonFunctionsByKindAndName = new HashMap<>();
private final Map<FunctionSymbolKey, List<Span>> functionsBySignature = new HashMap<>();
}
private static final class ModuleExports {
private final Set<NameId> publicNameIds = new HashSet<>();
}
}

View File

@ -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<PbsToken> 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<PbsToken> tokens,
final FileId fileId,
final DiagnosticSink diagnostics) {
return new PbsBarrelParser(tokens, fileId, diagnostics).parseFile();
}
private PbsAst.BarrelFile parseFile() {
final var items = new ArrayList<PbsAst.BarrelItem>();
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<PbsAst.TypeRef>();
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<PbsAst.NamedTypeField>();
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) {
}
}

View File

@ -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<ParsedSourceFile>();
final Map<PbsModuleVisibilityValidator.ModuleCoordinates, MutableModuleUnit> 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);
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()))));
} break;
case "barrel":
break;
default:
}
}
final var modules = new ArrayList<PbsModuleVisibilityValidator.ModuleUnit>(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<String>();
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<PbsModuleVisibilityValidator.SourceFile> sources = new ArrayList<>();
private final ArrayList<PbsModuleVisibilityValidator.BarrelFile> barrels = new ArrayList<>();
}
private record ParsedSourceFile(
FileId fileId,
PbsAst.File ast) {
}
}

View File

@ -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<String> sourceContents,
final String barrelContent,
final AtomicInteger nextFileId,
final DiagnosticSink diagnostics) {
final var sources = new ArrayList<PbsModuleVisibilityValidator.SourceFile>();
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<PbsModuleVisibilityValidator.BarrelFile>();
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<String> modulePathSegments(final String modulePath) {
if (modulePath == null || modulePath.isBlank()) {
return ReadOnlyList.empty();
}
return ReadOnlyList.wrap(List.of(modulePath.split("/")));
}
}

View File

@ -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<Err> (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));
}
}