From 2003fc749e7b6bf3775d76e13ce62d45de7c9656 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 26 Mar 2026 19:21:34 +0000 Subject: [PATCH] implements PR-19.5 global dependency graph and cycle validation --- .../compiler/pbs/PbsFrontendCompiler.java | 36 +++- .../PbsDeclarationSemanticsValidator.java | 13 ++ .../semantics/PbsGlobalDependencySupport.java | 76 ++++++++ .../PbsGlobalSemanticsValidator.java | 111 ++++++++++++ .../pbs/semantics/PbsSemanticsErrors.java | 1 + .../services/PBSFrontendPhaseService.java | 1 + .../services/PbsImportedSemanticContext.java | 7 +- .../PbsImportedSemanticContextService.java | 164 ++++++++++++++++++ .../PbsGlobalSemanticsValidatorTest.java | 82 +++++++++ .../services/PBSFrontendPhaseServiceTest.java | 59 +++++++ 10 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalDependencySupport.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidator.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidatorTest.java diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java index 1b517344..4a8a29b3 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java @@ -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 supplementalTopDecls, final ReadOnlyList importedCallables, + final ReadOnlyList importedGlobals, final IRReservedMetadata importedReservedMetadata) { final var effectiveModuleId = moduleId == null ? ModuleId.none() : moduleId; final var effectiveModulePool = modulePool == null ? ReadOnlyList.empty() : modulePool; @@ -114,11 +116,19 @@ public final class PbsFrontendCompiler { final var effectiveImportedCallables = importedCallables == null ? ReadOnlyList.empty() : importedCallables; + final var effectiveImportedGlobals = importedGlobals == null + ? ReadOnlyList.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 importedDependencies, + p.studio.compiler.source.Span span, + boolean directlyVisible) { + public ImportedGlobalSurface { + ownerModuleId = ownerModuleId == null ? ModuleId.none() : ownerModuleId; + importedDependencies = importedDependencies == null ? ReadOnlyList.empty() : importedDependencies; + } + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java index 88eb2843..e18997e3 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java @@ -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 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); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalDependencySupport.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalDependencySupport.java new file mode 100644 index 00000000..deb9dc4a --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalDependencySupport.java @@ -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 collectDependencies( + final PbsAst.Expression expression, + final ModuleId currentModuleId, + final Set localGlobals, + final Map importedGlobalsByLocalName) { + final var dependencies = new LinkedHashSet(); + collectRecursive(expression, currentModuleId, localGlobals, importedGlobalsByLocalName, dependencies); + return dependencies; + } + + private static void collectRecursive( + final PbsAst.Expression expression, + final ModuleId currentModuleId, + final Set localGlobals, + final Map importedGlobalsByLocalName, + final LinkedHashSet 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); + } + } + } +} + diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidator.java new file mode 100644 index 00000000..29124df9 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidator.java @@ -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 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(); + final var directlyVisibleImports = new LinkedHashMap(); + + 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>(); + 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>(); + final var spansByRef = new LinkedHashMap(); + + 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(); + 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( + 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 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 collectLocalGlobalDecls(final PbsAst.File ast) { + final var globals = new LinkedHashMap(); + for (final var topDecl : ast.topDecls()) { + if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { + globals.putIfAbsent(globalDecl.name(), globalDecl); + } + } + return globals; + } + + record Analysis( + DependencyGraphAnaliser.Analysis graphAnalysis, + Map spansByRef) { + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java index 68f26954..d52a78ac 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java @@ -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, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 24e81385..c402b51f 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -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()); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContext.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContext.java index 0b2a838e..0f72efc2 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContext.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContext.java @@ -8,8 +8,13 @@ import p.studio.utilities.structures.ReadOnlyList; record PbsImportedSemanticContext( ReadOnlyList supplementalTopDecls, ReadOnlyList importedCallables, + ReadOnlyList 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()); } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContextService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContextService.java index a8855723..71f3e884 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContextService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsImportedSemanticContextService.java @@ -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 contexts = new HashMap<>(); for (final var parsedSource : parsedSourceFiles) { final var supplementalTopDecls = new ArrayList(); final var importedCallables = new ArrayList(); + final var importedGlobals = new ArrayList(); final var importedCallableKeys = new HashSet(); + final var importedGlobalKeys = new HashSet(); final var supplementalKeys = new HashSet(); var importedReservedMetadata = IRReservedMetadata.empty(); + final var directlyVisibleGlobalRefs = new LinkedHashMap(); 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 importedGlobals, + final Set 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 indexGlobalInfos( + final Map> sourcesByModule, + final Map>> topDeclsByNameByModule, + final ModuleTable moduleTable) { + final Map 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 directImportedGlobalsFor( + final PbsAst.File ast, + final Map>> topDeclsByNameByModule, + final ModuleTable moduleTable) { + final Map 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 collectReachableImportedGlobals( + final Map directlyVisibleGlobalRefs, + final Map globalInfosByRef) { + final var visited = new LinkedHashSet(); + final var pending = new ArrayDeque(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(); + 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(); + 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 importedGlobalsByLocalName) { + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidatorTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidatorTest.java new file mode 100644 index 00000000..3a2624cb --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsGlobalSemanticsValidatorTest.java @@ -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; + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java index 432df120..e4a0e82c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java @@ -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,