implements PR006
This commit is contained in:
parent
931c5d96a3
commit
f60346e174
@ -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`.
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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<>();
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
},
|
||||
() -> 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<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) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -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("/")));
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user