implements PR-19.5 global dependency graph and cycle validation

This commit is contained in:
bQUARKz 2026-03-26 19:21:34 +00:00
parent 71a993ad4a
commit 2003fc749e
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 548 additions and 2 deletions

View File

@ -90,6 +90,7 @@ public final class PbsFrontendCompiler {
nameTable,
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty(),
IRReservedMetadata.empty());
}
@ -104,6 +105,7 @@ public final class PbsFrontendCompiler {
final NameTable nameTable,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final ReadOnlyList<ImportedCallableSurface> importedCallables,
final ReadOnlyList<ImportedGlobalSurface> importedGlobals,
final IRReservedMetadata importedReservedMetadata) {
final var effectiveModuleId = moduleId == null ? ModuleId.none() : moduleId;
final var effectiveModulePool = modulePool == null ? ReadOnlyList.<ModuleReference>empty() : modulePool;
@ -114,11 +116,19 @@ public final class PbsFrontendCompiler {
final var effectiveImportedCallables = importedCallables == null
? ReadOnlyList.<ImportedCallableSurface>empty()
: importedCallables;
final var effectiveImportedGlobals = importedGlobals == null
? ReadOnlyList.<ImportedGlobalSurface>empty()
: importedGlobals;
final var effectiveImportedReservedMetadata = importedReservedMetadata == null
? IRReservedMetadata.empty()
: importedReservedMetadata;
final var semanticsErrorBaseline = diagnostics.errorCount();
new PbsDeclarationSemanticsValidator(effectiveNameTable).validate(ast, sourceKind, diagnostics);
new PbsDeclarationSemanticsValidator(effectiveNameTable).validate(
ast,
sourceKind,
effectiveModuleId,
effectiveImportedGlobals,
diagnostics);
flowSemanticsValidator.validate(ast, effectiveSupplementalTopDecls, diagnostics);
if (diagnostics.errorCount() > semanticsErrorBaseline) {
return IRBackendFile.empty(fileId);
@ -233,4 +243,28 @@ public final class PbsFrontendCompiler {
moduleId = moduleId == null ? ModuleId.none() : moduleId;
}
}
public record ImportedGlobalDependency(
String localName,
ModuleId ownerModuleId,
String globalName) {
public ImportedGlobalDependency {
ownerModuleId = ownerModuleId == null ? ModuleId.none() : ownerModuleId;
}
}
public record ImportedGlobalSurface(
ModuleId ownerModuleId,
String globalName,
String localName,
PbsAst.TypeRef explicitType,
PbsAst.Expression initializer,
ReadOnlyList<ImportedGlobalDependency> importedDependencies,
p.studio.compiler.source.Span span,
boolean directlyVisible) {
public ImportedGlobalSurface {
ownerModuleId = ownerModuleId == null ? ModuleId.none() : ownerModuleId;
importedDependencies = importedDependencies == null ? ReadOnlyList.empty() : importedDependencies;
}
}
}

View File

@ -1,9 +1,11 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.PbsFrontendCompiler;
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.identifiers.ModuleId;
import p.studio.compiler.source.tables.NameTable;
import p.studio.utilities.structures.ReadOnlyList;
@ -25,6 +27,7 @@ public final class PbsDeclarationSemanticsValidator {
private final NameTable nameTable;
private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator();
private final PbsGlobalSemanticsValidator globalSemanticsValidator = new PbsGlobalSemanticsValidator();
public PbsDeclarationSemanticsValidator(final NameTable nameTable) {
this.nameTable = nameTable == null ? new NameTable() : nameTable;
@ -38,6 +41,15 @@ public final class PbsDeclarationSemanticsValidator {
final PbsAst.File ast,
final SourceKind sourceKind,
final DiagnosticSink diagnostics) {
validate(ast, sourceKind, ModuleId.none(), ReadOnlyList.empty(), diagnostics);
}
public void validate(
final PbsAst.File ast,
final SourceKind sourceKind,
final ModuleId currentModuleId,
final ReadOnlyList<PbsFrontendCompiler.ImportedGlobalSurface> importedGlobals,
final DiagnosticSink diagnostics) {
final var binder = new PbsNamespaceBinder(nameTable, diagnostics);
final var rules = new PbsDeclarationRuleValidator(nameTable, diagnostics);
final var interfaceModule = sourceKind == SourceKind.SDK_INTERFACE;
@ -153,6 +165,7 @@ public final class PbsDeclarationSemanticsValidator {
}
constSemanticsValidator.validate(ast, diagnostics);
globalSemanticsValidator.validate(ast, currentModuleId, importedGlobals, diagnostics);
if (interfaceModule) {
PbsBuiltinLayoutResolver.resolve(ast, diagnostics);
}

View File

@ -0,0 +1,76 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.identifiers.ModuleId;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
public final class PbsGlobalDependencySupport {
private PbsGlobalDependencySupport() {
}
public record GlobalRef(
ModuleId moduleId,
String globalName) {
public GlobalRef {
moduleId = moduleId == null ? ModuleId.none() : moduleId;
}
}
public static LinkedHashSet<GlobalRef> collectDependencies(
final PbsAst.Expression expression,
final ModuleId currentModuleId,
final Set<String> localGlobals,
final Map<String, GlobalRef> importedGlobalsByLocalName) {
final var dependencies = new LinkedHashSet<GlobalRef>();
collectRecursive(expression, currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies);
return dependencies;
}
private static void collectRecursive(
final PbsAst.Expression expression,
final ModuleId currentModuleId,
final Set<String> localGlobals,
final Map<String, GlobalRef> importedGlobalsByLocalName,
final LinkedHashSet<GlobalRef> dependencies) {
if (expression == null) {
return;
}
if (expression instanceof PbsAst.IdentifierExpr identifierExpr) {
if (localGlobals.contains(identifierExpr.name())) {
dependencies.add(new GlobalRef(currentModuleId, identifierExpr.name()));
return;
}
final var importedGlobal = importedGlobalsByLocalName.get(identifierExpr.name());
if (importedGlobal != null) {
dependencies.add(importedGlobal);
}
return;
}
if (expression instanceof PbsAst.GroupExpr groupExpr) {
collectRecursive(groupExpr.expression(), currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies);
return;
}
if (expression instanceof PbsAst.UnaryExpr unaryExpr) {
collectRecursive(unaryExpr.expression(), currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies);
return;
}
if (expression instanceof PbsAst.BinaryExpr binaryExpr) {
collectRecursive(binaryExpr.left(), currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies);
collectRecursive(binaryExpr.right(), currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies);
return;
}
if (expression instanceof PbsAst.MemberExpr memberExpr) {
collectRecursive(memberExpr.receiver(), currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies);
return;
}
if (expression instanceof PbsAst.NewExpr newExpr) {
for (final var argument : newExpr.arguments()) {
collectRecursive(argument, currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies);
}
}
}
}

View File

@ -0,0 +1,111 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.PbsFrontendCompiler;
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.identifiers.ModuleId;
import p.studio.utilities.structures.DependencyGraphAnaliser;
import java.util.*;
final class PbsGlobalSemanticsValidator {
Analysis analyse(
final PbsAst.File ast,
final ModuleId currentModuleId,
final p.studio.utilities.structures.ReadOnlyList<PbsFrontendCompiler.ImportedGlobalSurface> importedGlobals) {
final var normalizedModuleId = currentModuleId == null ? ModuleId.none() : currentModuleId;
final var localDeclsByName = collectLocalGlobalDecls(ast);
final var localGlobalNames = new LinkedHashSet<>(localDeclsByName.keySet());
final var importedDefinitionsByRef = new LinkedHashMap<PbsGlobalDependencySupport.GlobalRef, PbsFrontendCompiler.ImportedGlobalSurface>();
final var directlyVisibleImports = new LinkedHashMap<String, PbsGlobalDependencySupport.GlobalRef>();
for (final var importedGlobal : importedGlobals) {
final var ref = new PbsGlobalDependencySupport.GlobalRef(importedGlobal.ownerModuleId(), importedGlobal.globalName());
importedDefinitionsByRef.putIfAbsent(ref, importedGlobal);
if (importedGlobal.directlyVisible()) {
directlyVisibleImports.putIfAbsent(importedGlobal.localName(), ref);
}
}
final var globalNamesByModule = new LinkedHashMap<ModuleId, LinkedHashSet<String>>();
globalNamesByModule.put(normalizedModuleId, localGlobalNames);
for (final var ref : importedDefinitionsByRef.keySet()) {
globalNamesByModule.computeIfAbsent(ref.moduleId(), ignored -> new LinkedHashSet<>()).add(ref.globalName());
}
final var graph = new LinkedHashMap<PbsGlobalDependencySupport.GlobalRef, Set<PbsGlobalDependencySupport.GlobalRef>>();
final var spansByRef = new LinkedHashMap<PbsGlobalDependencySupport.GlobalRef, Span>();
for (final var entry : localDeclsByName.entrySet()) {
final var ref = new PbsGlobalDependencySupport.GlobalRef(normalizedModuleId, entry.getKey());
spansByRef.put(ref, entry.getValue().span());
graph.put(ref, PbsGlobalDependencySupport.collectDependencies(
entry.getValue().initializer(),
normalizedModuleId,
localGlobalNames,
directlyVisibleImports));
}
for (final var entry : importedDefinitionsByRef.entrySet()) {
final var surface = entry.getValue();
spansByRef.putIfAbsent(entry.getKey(), surface.span());
final var importedByLocalName = new LinkedHashMap<String, PbsGlobalDependencySupport.GlobalRef>();
for (final var importedDependency : surface.importedDependencies()) {
importedByLocalName.putIfAbsent(
importedDependency.localName(),
new PbsGlobalDependencySupport.GlobalRef(
importedDependency.ownerModuleId(),
importedDependency.globalName()));
}
graph.putIfAbsent(entry.getKey(), PbsGlobalDependencySupport.collectDependencies(
surface.initializer(),
surface.ownerModuleId(),
globalNamesByModule.getOrDefault(surface.ownerModuleId(), new LinkedHashSet<>()),
importedByLocalName));
}
final var analysis = new DependencyGraphAnaliser<PbsGlobalDependencySupport.GlobalRef>(
Comparator.comparingInt((PbsGlobalDependencySupport.GlobalRef ref) -> ref.moduleId().isNone() ? Integer.MAX_VALUE : ref.moduleId().getIndex())
.thenComparing(PbsGlobalDependencySupport.GlobalRef::globalName))
.analyse(graph);
return new Analysis(analysis, spansByRef);
}
void validate(
final PbsAst.File ast,
final ModuleId currentModuleId,
final p.studio.utilities.structures.ReadOnlyList<PbsFrontendCompiler.ImportedGlobalSurface> importedGlobals,
final DiagnosticSink diagnostics) {
final var analysis = analyse(ast, currentModuleId, importedGlobals);
for (final var cycle : analysis.graphAnalysis().cycleComponents()) {
for (final var ref : cycle) {
final var span = analysis.spansByRef().get(ref);
if (span == null) {
continue;
}
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_GLOBAL_CYCLIC_DEPENDENCY.name(),
"Cyclic global dependency detected involving '%s'".formatted(ref.globalName()),
span);
}
}
}
private LinkedHashMap<String, PbsAst.GlobalDecl> collectLocalGlobalDecls(final PbsAst.File ast) {
final var globals = new LinkedHashMap<String, PbsAst.GlobalDecl>();
for (final var topDecl : ast.topDecls()) {
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
globals.putIfAbsent(globalDecl.name(), globalDecl);
}
}
return globals;
}
record Analysis(
DependencyGraphAnaliser.Analysis<PbsGlobalDependencySupport.GlobalRef> graphAnalysis,
Map<PbsGlobalDependencySupport.GlobalRef, Span> spansByRef) {
}
}

View File

@ -20,6 +20,7 @@ public enum PbsSemanticsErrors {
E_SEM_MISSING_GLOBAL_TYPE_ANNOTATION,
E_SEM_MISSING_GLOBAL_INITIALIZER,
E_SEM_GLOBAL_UNSUPPORTED_INITIALIZER,
E_SEM_GLOBAL_CYCLIC_DEPENDENCY,
E_SEM_CONST_NON_CONSTANT_INITIALIZER,
E_SEM_CONST_INITIALIZER_TYPE_MISMATCH,
E_SEM_CONST_CYCLIC_DEPENDENCY,

View File

@ -76,6 +76,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
nameTable,
importedSemanticContext.supplementalTopDecls(),
importedSemanticContext.importedCallables(),
importedSemanticContext.importedGlobals(),
importedSemanticContext.importedReservedMetadata());
if (diagnostics.errorCount() > compileErrorBaseline) {
failedModuleIds.add(parsedSource.moduleId());

View File

@ -8,8 +8,13 @@ import p.studio.utilities.structures.ReadOnlyList;
record PbsImportedSemanticContext(
ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
ReadOnlyList<PbsFrontendCompiler.ImportedCallableSurface> importedCallables,
ReadOnlyList<PbsFrontendCompiler.ImportedGlobalSurface> importedGlobals,
IRReservedMetadata importedReservedMetadata) {
static PbsImportedSemanticContext empty() {
return new PbsImportedSemanticContext(ReadOnlyList.empty(), ReadOnlyList.empty(), IRReservedMetadata.empty());
return new PbsImportedSemanticContext(
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty(),
IRReservedMetadata.empty());
}
}

View File

@ -4,6 +4,7 @@ import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsGlobalDependencySupport;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.ModuleTable;
@ -46,14 +47,18 @@ final class PbsImportedSemanticContextService {
for (final var entry : sourcesByModule.entrySet()) {
topDeclsByNameByModule.put(entry.getKey(), indexTopDeclsByName(entry.getValue()));
}
final var globalInfosByRef = indexGlobalInfos(sourcesByModule, topDeclsByNameByModule, moduleTable);
final Map<FileId, PbsImportedSemanticContext> contexts = new HashMap<>();
for (final var parsedSource : parsedSourceFiles) {
final var supplementalTopDecls = new ArrayList<PbsAst.TopDecl>();
final var importedCallables = new ArrayList<PbsFrontendCompiler.ImportedCallableSurface>();
final var importedGlobals = new ArrayList<PbsFrontendCompiler.ImportedGlobalSurface>();
final var importedCallableKeys = new HashSet<String>();
final var importedGlobalKeys = new HashSet<String>();
final var supplementalKeys = new HashSet<String>();
var importedReservedMetadata = IRReservedMetadata.empty();
final var directlyVisibleGlobalRefs = new LinkedHashMap<String, PbsGlobalDependencySupport.GlobalRef>();
for (final var importDecl : parsedSource.ast().imports()) {
final var moduleRef = importDecl.moduleRef();
@ -102,14 +107,43 @@ final class PbsImportedSemanticContextService {
functionDecl.parameters().size(),
returnSlotsFor(functionDecl),
frontendCompiler.callableShapeSurfaceOf(functionDecl)));
continue;
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
directlyVisibleGlobalRefs.putIfAbsent(
localName,
new PbsGlobalDependencySupport.GlobalRef(importedModuleId, globalDecl.name()));
}
}
}
}
final var reachableImportedGlobals = collectReachableImportedGlobals(directlyVisibleGlobalRefs, globalInfosByRef);
for (final var entry : directlyVisibleGlobalRefs.entrySet()) {
final var info = globalInfosByRef.get(entry.getValue());
if (info == null) {
continue;
}
appendImportedGlobal(
importedGlobals,
importedGlobalKeys,
toImportedGlobalSurface(info, entry.getKey(), true));
}
for (final var reachableRef : reachableImportedGlobals) {
final var info = globalInfosByRef.get(reachableRef);
if (info == null) {
continue;
}
appendImportedGlobal(
importedGlobals,
importedGlobalKeys,
toImportedGlobalSurface(info, info.globalDecl().name(), false));
}
contexts.put(parsedSource.fileId(), new PbsImportedSemanticContext(
ReadOnlyList.wrap(supplementalTopDecls),
ReadOnlyList.wrap(importedCallables),
ReadOnlyList.wrap(importedGlobals),
importedReservedMetadata));
}
@ -212,6 +246,10 @@ final class PbsImportedSemanticContextService {
}
if (topDecl instanceof PbsAst.ConstDecl constDecl && constDecl.explicitType() != null) {
collectReferencedTypeNames(constDecl.explicitType(), sink);
return;
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl && globalDecl.explicitType() != null) {
collectReferencedTypeNames(globalDecl.explicitType(), sink);
}
}
@ -319,6 +357,22 @@ final class PbsImportedSemanticContextService {
}
}
private void appendImportedGlobal(
final ArrayList<PbsFrontendCompiler.ImportedGlobalSurface> importedGlobals,
final Set<String> importedGlobalKeys,
final PbsFrontendCompiler.ImportedGlobalSurface importedGlobalSurface) {
final var globalKey = importedGlobalSurface.ownerModuleId().getIndex()
+ "#"
+ importedGlobalSurface.globalName()
+ "#"
+ importedGlobalSurface.localName()
+ "#"
+ importedGlobalSurface.directlyVisible();
if (importedGlobalKeys.add(globalKey)) {
importedGlobals.add(importedGlobalSurface);
}
}
private String topDeclKey(final PbsAst.TopDecl topDecl) {
final var declName = topDeclName(topDecl);
if (declName == null || declName.isBlank()) {
@ -337,6 +391,9 @@ final class PbsImportedSemanticContextService {
if (topDecl instanceof PbsAst.ConstDecl constDecl) {
return constDecl.name();
}
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
return globalDecl.name();
}
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
return serviceDecl.name();
}
@ -400,4 +457,111 @@ final class PbsImportedSemanticContextService {
}
return importItem.alias();
}
private Map<PbsGlobalDependencySupport.GlobalRef, GlobalInfo> indexGlobalInfos(
final Map<ModuleId, ArrayList<PbsParsedSourceFile>> sourcesByModule,
final Map<ModuleId, Map<String, ArrayList<PbsAst.TopDecl>>> topDeclsByNameByModule,
final ModuleTable moduleTable) {
final Map<PbsGlobalDependencySupport.GlobalRef, GlobalInfo> globalsByRef = new LinkedHashMap<>();
for (final var entry : sourcesByModule.entrySet()) {
final var moduleId = entry.getKey();
for (final var source : entry.getValue()) {
final var importedGlobalsByLocalName = directImportedGlobalsFor(source.ast(), topDeclsByNameByModule, moduleTable);
for (final var topDecl : source.ast().topDecls()) {
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
globalsByRef.putIfAbsent(
new PbsGlobalDependencySupport.GlobalRef(moduleId, globalDecl.name()),
new GlobalInfo(moduleId, globalDecl, importedGlobalsByLocalName));
}
}
}
}
return globalsByRef;
}
private Map<String, PbsGlobalDependencySupport.GlobalRef> directImportedGlobalsFor(
final PbsAst.File ast,
final Map<ModuleId, Map<String, ArrayList<PbsAst.TopDecl>>> topDeclsByNameByModule,
final ModuleTable moduleTable) {
final Map<String, PbsGlobalDependencySupport.GlobalRef> importedGlobalsByLocalName = new LinkedHashMap<>();
for (final var importDecl : ast.imports()) {
final var importedModuleId = moduleTable.register(
new p.studio.compiler.source.tables.ModuleReference(
importDecl.moduleRef().project(),
importDecl.moduleRef().pathSegments()));
final var importedTopDeclsByName = topDeclsByNameByModule.get(importedModuleId);
if (importedTopDeclsByName == null) {
continue;
}
for (final var importItem : importDecl.items()) {
final var candidates = importedTopDeclsByName.getOrDefault(importItem.name(), new ArrayList<>());
for (final var topDecl : candidates) {
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
importedGlobalsByLocalName.putIfAbsent(
importItemLocalName(importItem),
new PbsGlobalDependencySupport.GlobalRef(importedModuleId, globalDecl.name()));
break;
}
}
}
}
return importedGlobalsByLocalName;
}
private LinkedHashSet<PbsGlobalDependencySupport.GlobalRef> collectReachableImportedGlobals(
final Map<String, PbsGlobalDependencySupport.GlobalRef> directlyVisibleGlobalRefs,
final Map<PbsGlobalDependencySupport.GlobalRef, GlobalInfo> globalInfosByRef) {
final var visited = new LinkedHashSet<PbsGlobalDependencySupport.GlobalRef>();
final var pending = new ArrayDeque<PbsGlobalDependencySupport.GlobalRef>(directlyVisibleGlobalRefs.values());
while (!pending.isEmpty()) {
final var current = pending.removeFirst();
if (!visited.add(current)) {
continue;
}
final var info = globalInfosByRef.get(current);
if (info == null) {
continue;
}
final var globalsInSameModule = new LinkedHashSet<String>();
for (final var entry : globalInfosByRef.entrySet()) {
if (entry.getKey().moduleId().equals(info.moduleId())) {
globalsInSameModule.add(entry.getKey().globalName());
}
}
pending.addAll(PbsGlobalDependencySupport.collectDependencies(
info.globalDecl().initializer(),
info.moduleId(),
globalsInSameModule,
info.importedGlobalsByLocalName()));
}
return visited;
}
private PbsFrontendCompiler.ImportedGlobalSurface toImportedGlobalSurface(
final GlobalInfo info,
final String localName,
final boolean directlyVisible) {
final var importedDependencies = new ArrayList<PbsFrontendCompiler.ImportedGlobalDependency>();
for (final var entry : info.importedGlobalsByLocalName().entrySet()) {
importedDependencies.add(new PbsFrontendCompiler.ImportedGlobalDependency(
entry.getKey(),
entry.getValue().moduleId(),
entry.getValue().globalName()));
}
return new PbsFrontendCompiler.ImportedGlobalSurface(
info.moduleId(),
info.globalDecl().name(),
localName,
info.globalDecl().explicitType(),
info.globalDecl().initializer(),
ReadOnlyList.wrap(importedDependencies),
info.globalDecl().span(),
directlyVisible);
}
private record GlobalInfo(
ModuleId moduleId,
PbsAst.GlobalDecl globalDecl,
Map<String, PbsGlobalDependencySupport.GlobalRef> importedGlobalsByLocalName) {
}
}

View File

@ -0,0 +1,82 @@
package p.studio.compiler.pbs.semantics;
import org.junit.jupiter.api.Test;
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.parser.PbsParser;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsGlobalSemanticsValidatorTest {
@Test
void shouldProduceDeterministicGlobalOrderIndependentOfSourceOrder() {
final var firstAst = parse("""
declare global B: int = A + 1;
declare global C: int = B + 1;
declare global A: int = 1;
""");
final var secondAst = parse("""
declare global C: int = B + 1;
declare global A: int = 1;
declare global B: int = A + 1;
""");
final var validator = new PbsGlobalSemanticsValidator();
final var firstOrder = validator.analyse(firstAst, ModuleId.none(), ReadOnlyList.empty())
.graphAnalysis()
.traversalOrder()
.asList()
.stream()
.map(PbsGlobalDependencySupport.GlobalRef::globalName)
.toList();
final var secondOrder = validator.analyse(secondAst, ModuleId.none(), ReadOnlyList.empty())
.graphAnalysis()
.traversalOrder()
.asList()
.stream()
.map(PbsGlobalDependencySupport.GlobalRef::globalName)
.toList();
assertEquals(List.of("A", "B", "C"), firstOrder);
assertEquals(firstOrder, secondOrder);
}
@Test
void shouldRejectCyclicGlobalDependencies() {
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), """
declare global A: int = B + 1;
declare global B: int = C + 1;
declare global C: int = A + 1;
""", diagnostics);
final var cycleCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_GLOBAL_CYCLIC_DEPENDENCY.name()))
.count();
assertEquals(3, cycleCount);
assertFalse(diagnostics.isEmpty());
}
private PbsAst.File parse(final String source) {
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final var ast = PbsParser.parse(
PbsLexer.lex(source, fileId, diagnostics),
fileId,
diagnostics,
PbsParser.ParseMode.ORDINARY);
assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
return ast;
}
}

View File

@ -1168,6 +1168,65 @@ class PBSFrontendPhaseServiceTest {
assertTrue(intrinsicCalls.contains("input.touch.y"));
}
@Test
void shouldReportInterModuleGlobalCycleThroughImportedAlias() throws IOException {
final var projectRoot = tempDir.resolve("project-global-cycle-alias");
final var sourceRoot = projectRoot.resolve("src");
final var moduleAPath = sourceRoot.resolve("a");
final var moduleBPath = sourceRoot.resolve("b");
Files.createDirectories(moduleAPath);
Files.createDirectories(moduleBPath);
final var sourceA = moduleAPath.resolve("source.pbs");
final var barrelA = moduleAPath.resolve("mod.barrel");
Files.writeString(sourceA, """
import { B as ImportedB } from @app:b;
declare global A: int = ImportedB + 1;
""");
Files.writeString(barrelA, "pub global A;");
final var sourceB = moduleBPath.resolve("source.pbs");
final var barrelB = moduleBPath.resolve("mod.barrel");
Files.writeString(sourceB, """
import { A } from @app:a;
declare global B: int = A + 1;
""");
Files.writeString(barrelB, "pub global B;");
final var projectTable = new ProjectTable();
final var fileTable = new FileTable(1);
final var projectId = projectTable.register(ProjectDescriptor.builder()
.rootPath(projectRoot)
.name("app")
.version("1.0.0")
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
.build());
registerFile(projectId, projectRoot, sourceA, fileTable);
registerFile(projectId, projectRoot, barrelA, fileTable);
registerFile(projectId, projectRoot, sourceB, fileTable);
registerFile(projectId, projectRoot, barrelB, fileTable);
final var ctx = new FrontendPhaseContext(
projectTable,
fileTable,
new BuildStack(ReadOnlyList.wrap(List.of(projectId))));
final var diagnostics = DiagnosticSink.empty();
new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
final var cycleCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_GLOBAL_CYCLIC_DEPENDENCY.name()))
.count();
assertEquals(2, cycleCount, diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
}
private void registerFile(
final ProjectId projectId,
final Path projectRoot,