From f7abaf27d437d1485e32e2f76136e69483e362fc Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 6 Mar 2026 16:35:24 +0000 Subject: [PATCH] implements PR031 --- .../specs/12. Diagnostics Specification.md | 4 + .../13. Lowering IRBackend Specification.md | 6 + .../services/PBSFrontendPhaseService.java | 54 +++++- .../services/PBSFrontendPhaseServiceTest.java | 166 +++++++++++++++++- 4 files changed, 224 insertions(+), 6 deletions(-) diff --git a/docs/pbs/specs/12. Diagnostics Specification.md b/docs/pbs/specs/12. Diagnostics Specification.md index a11ecedc..07b0c9bd 100644 --- a/docs/pbs/specs/12. Diagnostics Specification.md +++ b/docs/pbs/specs/12. Diagnostics Specification.md @@ -178,6 +178,7 @@ At minimum, the PBS diagnostics baseline must cover: 4. linking failures required by source-level name-resolution and module-linking rules, 5. malformed, unauthorized, or capability-rejected host usage required by `6.2. Host ABI Binding and Loader Resolution Specification.md` and `7. Cartridge Manifest and Runtime Capabilities Specification.md`, 6. source-attributable backend-originated failures that remain user-actionable under normative lowering or load-facing rules. +7. dependency-scoped fail-fast admission at frontend boundary: when one module is rejected, modules that import it (directly or transitively) must not be emitted, while diagnostics collection for independent modules must continue. At minimum, host-admission diagnostics must cover missing or malformed host capability metadata and unknown or undeclared capability names. @@ -193,6 +194,9 @@ When a backend-originated failure remains in scope for PBS-facing diagnostics, v Dynamic-semantics traps are not source-level recoverable diagnostics in the ordinary PBS userland model. This document therefore does not require a compile-time diagnostic surface for every possible fatal runtime trap. +Dependency-scoped fail-fast admission is not equivalent to global build abort. +When failures exist, implementations must still preserve deterministic diagnostic identity and attribution for unaffected independent modules processed in the same build. + ## 11. Cost Diagnostics and Warning Policy Warnings are supported and recommended in v1, but they are not part of the minimum mandatory conformance baseline. diff --git a/docs/pbs/specs/13. Lowering IRBackend Specification.md b/docs/pbs/specs/13. Lowering IRBackend Specification.md index 72ed2dac..f7642ce7 100644 --- a/docs/pbs/specs/13. Lowering IRBackend Specification.md +++ b/docs/pbs/specs/13. Lowering IRBackend Specification.md @@ -91,6 +91,12 @@ If a source form is outside current frontend-lowering support: Lowering must not convert a required syntax/static rejection into accepted lowered behavior. +For multi-module builds, lowering admission must apply dependency-scoped fail-fast: + +1. a module rejected in syntax, static semantics, linking, host-admission, or load-facing gates must not be emitted; +2. any module that imports a rejected module (directly or transitively) must also be excluded from `IRBackend` emission; +3. modules independent from the rejected dependency subgraph may continue to lower and emit in the same build. + ## 8. Conformance Boundary `IRBackend` is the first lowering boundary (frontend responsibility). 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 17a8cd7a..ccda745e 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 @@ -134,10 +134,12 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { } moduleVisibilityValidator.validate(ReadOnlyList.wrap(modules), diagnostics); markModulesWithLinkingErrors(diagnostics, moduleKeyByFile, failedModuleKeys); + final var moduleDependencyGraph = buildModuleDependencyGraph(parsedSourceFiles); final var compiledSourceFiles = new ArrayList(parsedSourceFiles.size()); for (final var parsedSource : parsedSourceFiles) { - if (failedModuleKeys.contains(parsedSource.moduleKey())) { + final var blockedModuleKeys = blockedModulesByDependency(failedModuleKeys, moduleDependencyGraph); + if (blockedModuleKeys.contains(parsedSource.moduleKey())) { continue; } final var compileErrorBaseline = diagnostics.errorCount(); @@ -153,8 +155,9 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { compiledSourceFiles.add(new CompiledSourceFile(parsedSource.moduleKey(), irBackendFile)); } + final var blockedModuleKeys = blockedModulesByDependency(failedModuleKeys, moduleDependencyGraph); for (final var compiledSource : compiledSourceFiles) { - if (failedModuleKeys.contains(compiledSource.moduleKey())) { + if (blockedModuleKeys.contains(compiledSource.moduleKey())) { continue; } irBackendAggregator.merge(compiledSource.irBackendFile()); @@ -306,10 +309,57 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { } } + private Map> buildModuleDependencyGraph( + final ArrayList parsedSourceFiles) { + final Map> dependenciesByModule = new HashMap<>(); + for (final var parsedSource : parsedSourceFiles) { + final var moduleDependencies = dependenciesByModule.computeIfAbsent( + parsedSource.moduleKey(), + ignored -> new HashSet<>()); + for (final var importDecl : parsedSource.ast().imports()) { + final var moduleRef = importDecl.moduleRef(); + moduleDependencies.add(moduleKey(moduleRef.project(), moduleRef.pathSegments())); + } + } + return dependenciesByModule; + } + + private Set blockedModulesByDependency( + final Set failedModuleKeys, + final Map> dependenciesByModule) { + final Map> dependentsByModule = new HashMap<>(); + for (final var entry : dependenciesByModule.entrySet()) { + final var importer = entry.getKey(); + for (final var dependency : entry.getValue()) { + dependentsByModule.computeIfAbsent(dependency, ignored -> new HashSet<>()).add(importer); + } + } + + final var blocked = new HashSet(failedModuleKeys); + final var pending = new ArrayDeque(failedModuleKeys); + while (!pending.isEmpty()) { + final var failedOrBlocked = pending.removeFirst(); + final var dependents = dependentsByModule.getOrDefault(failedOrBlocked, Set.of()); + for (final var dependent : dependents) { + if (!blocked.add(dependent)) { + continue; + } + pending.addLast(dependent); + } + } + return blocked; + } + private String moduleKey(final PbsModuleVisibilityValidator.ModuleCoordinates coordinates) { return coordinates.project() + ":" + String.join("/", coordinates.pathSegments().asList()); } + private String moduleKey( + final String project, + final ReadOnlyList pathSegments) { + return project + ":" + String.join("/", pathSegments.asList()); + } + private static final class MutableModuleUnit { private final ArrayList sources = new ArrayList<>(); private final ArrayList barrels = new ArrayList<>(); 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 135760fc..284d29e9 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 @@ -8,6 +8,7 @@ import p.studio.compiler.models.BuildStack; import p.studio.compiler.models.ProjectDescriptor; import p.studio.compiler.models.SourceHandle; import p.studio.compiler.models.SourceKind; +import p.studio.compiler.pbs.PbsHostAdmissionErrors; import p.studio.compiler.pbs.stdlib.InterfaceModuleLoader; import p.studio.compiler.pbs.stdlib.StdlibEnvironment; import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver; @@ -294,7 +295,7 @@ class PBSFrontendPhaseServiceTest { final var sourceFile = modulePath.resolve("source.pbs"); final var modBarrel = modulePath.resolve("mod.barrel"); Files.writeString(sourceFile, """ - import { draw } from @sdk:gfx; + import { Gfx } from @sdk:gfx; fn use() -> int { return 1; } """); Files.writeString(modBarrel, "pub fn use() -> int;"); @@ -314,8 +315,16 @@ class PBSFrontendPhaseServiceTest { final var stdlibModule = new StdlibModuleSource( "sdk", ReadOnlyList.wrap(List.of("gfx")), - ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile("gfx.pbs", "fn draw() -> int { return 1; }"))), - "pub fn draw() -> int;"); + ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile( + "gfx.pbs", + """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] + fn draw(x: int, y: int) -> void; + } + """))), + "pub host Gfx;"); final var frontendService = new PBSFrontendPhaseService( resolverForMajor(7, stdlibModule), new InterfaceModuleLoader(2_000_000)); @@ -621,8 +630,157 @@ class PBSFrontendPhaseServiceTest { .orElseThrow(); assertEquals(p.studio.compiler.source.diagnostics.DiagnosticPhase.STATIC_SEMANTICS, semanticsDiagnostic.getPhase()); assertEquals(semanticsDiagnostic.getCode(), semanticsDiagnostic.getTemplateId()); + assertEquals(0, irBackend.getFunctions().size()); + } + + @Test + void shouldBlockDependentModulesTransitivelyWhenUpstreamModuleFailsLinking() throws IOException { + final var projectRoot = tempDir.resolve("project-fail-fast-linking"); + final var sourceRoot = projectRoot.resolve("src"); + final var moduleAPath = sourceRoot.resolve("a"); + final var moduleBPath = sourceRoot.resolve("b"); + final var moduleCPath = sourceRoot.resolve("c"); + final var moduleDPath = sourceRoot.resolve("d"); + Files.createDirectories(moduleAPath); + Files.createDirectories(moduleBPath); + Files.createDirectories(moduleCPath); + Files.createDirectories(moduleDPath); + + final var aSource = moduleAPath.resolve("source.pbs"); + final var aBarrel = moduleAPath.resolve("mod.barrel"); + final var aInvalidBarrel = moduleAPath.resolve("extra.barrel"); + Files.writeString(aSource, "fn a_entry() -> int { return 1; }"); + Files.writeString(aBarrel, "pub fn a_entry() -> int;"); + Files.writeString(aInvalidBarrel, "pub fn a_entry() -> int;"); + + final var bSource = moduleBPath.resolve("source.pbs"); + final var bBarrel = moduleBPath.resolve("mod.barrel"); + Files.writeString(bSource, """ + import { a_entry } from @app:a; + fn b_entry() -> int { return 2; } + """); + Files.writeString(bBarrel, "pub fn b_entry() -> int;"); + + final var cSource = moduleCPath.resolve("source.pbs"); + final var cBarrel = moduleCPath.resolve("mod.barrel"); + Files.writeString(cSource, """ + import { b_entry } from @app:b; + fn c_entry() -> int { return 3; } + """); + Files.writeString(cBarrel, "pub fn c_entry() -> int;"); + + final var dSource = moduleDPath.resolve("source.pbs"); + final var dBarrel = moduleDPath.resolve("mod.barrel"); + Files.writeString(dSource, "fn d_entry() -> int { return 4; }"); + Files.writeString(dBarrel, "pub fn d_entry() -> int;"); + + 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, aSource, fileTable); + registerFile(projectId, projectRoot, aBarrel, fileTable); + registerFile(projectId, projectRoot, aInvalidBarrel, fileTable); + registerFile(projectId, projectRoot, bSource, fileTable); + registerFile(projectId, projectRoot, bBarrel, fileTable); + registerFile(projectId, projectRoot, cSource, fileTable); + registerFile(projectId, projectRoot, cBarrel, fileTable); + registerFile(projectId, projectRoot, dSource, fileTable); + registerFile(projectId, projectRoot, dBarrel, fileTable); + + final var ctx = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId)))); + final var diagnostics = DiagnosticSink.empty(); + + final var irBackend = new PBSFrontendPhaseService().compile( + ctx, + diagnostics, + LogAggregator.empty(), + BuildingIssueSink.empty()); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_INVALID_BARREL_FILENAME.name()))); assertEquals(1, irBackend.getFunctions().size()); - assertEquals("run", irBackend.getFunctions().getFirst().name()); + assertEquals("d_entry", irBackend.getFunctions().getFirst().name()); + } + + @Test + void shouldBlockDependentModulesWhenUpstreamModuleFailsHostAdmission() throws IOException { + final var projectRoot = tempDir.resolve("project-fail-fast-host-admission"); + final var sourceRoot = projectRoot.resolve("src"); + final var dependentModulePath = sourceRoot.resolve("dependent"); + final var independentModulePath = sourceRoot.resolve("independent"); + Files.createDirectories(dependentModulePath); + Files.createDirectories(independentModulePath); + + final var dependentSource = dependentModulePath.resolve("source.pbs"); + final var dependentBarrel = dependentModulePath.resolve("mod.barrel"); + Files.writeString(dependentSource, """ + import { Gfx } from @sdk:gfx; + fn dependent() -> int { return 2; } + """); + Files.writeString(dependentBarrel, "pub fn dependent() -> int;"); + + final var independentSource = independentModulePath.resolve("source.pbs"); + final var independentBarrel = independentModulePath.resolve("mod.barrel"); + Files.writeString(independentSource, "fn independent() -> int { return 3; }"); + Files.writeString(independentBarrel, "pub fn independent() -> int;"); + + 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, dependentSource, fileTable); + registerFile(projectId, projectRoot, dependentBarrel, fileTable); + registerFile(projectId, projectRoot, independentSource, fileTable); + registerFile(projectId, projectRoot, independentBarrel, fileTable); + + final var badSdkModule = new StdlibModuleSource( + "sdk", + ReadOnlyList.wrap(List.of("gfx")), + ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile( + "main.pbs", + """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "unknown_capability")] + fn draw_pixel(x: int, y: int) -> void; + } + """))), + "pub host Gfx;"); + final var frontendService = new PBSFrontendPhaseService( + resolverForMajor(9, badSdkModule), + new InterfaceModuleLoader(3_000_000)); + + final var ctx = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId))), + 9); + final var diagnostics = DiagnosticSink.empty(); + + final var irBackend = frontendService.compile( + ctx, + diagnostics, + LogAggregator.empty(), + BuildingIssueSink.empty()); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsHostAdmissionErrors.E_HOST_UNKNOWN_CAPABILITY.name()))); + assertEquals(1, irBackend.getFunctions().size()); + assertEquals("independent", irBackend.getFunctions().getFirst().name()); } private void registerFile(