implements PR031

This commit is contained in:
bQUARKz 2026-03-06 16:35:24 +00:00
parent 495104db6d
commit f7abaf27d4
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 224 additions and 6 deletions

View File

@ -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, 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`, 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. 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. 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. 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 ## 11. Cost Diagnostics and Warning Policy
Warnings are supported and recommended in v1, but they are not part of the minimum mandatory conformance baseline. Warnings are supported and recommended in v1, but they are not part of the minimum mandatory conformance baseline.

View File

@ -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. 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 ## 8. Conformance Boundary
`IRBackend` is the first lowering boundary (frontend responsibility). `IRBackend` is the first lowering boundary (frontend responsibility).

View File

@ -134,10 +134,12 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
} }
moduleVisibilityValidator.validate(ReadOnlyList.wrap(modules), diagnostics); moduleVisibilityValidator.validate(ReadOnlyList.wrap(modules), diagnostics);
markModulesWithLinkingErrors(diagnostics, moduleKeyByFile, failedModuleKeys); markModulesWithLinkingErrors(diagnostics, moduleKeyByFile, failedModuleKeys);
final var moduleDependencyGraph = buildModuleDependencyGraph(parsedSourceFiles);
final var compiledSourceFiles = new ArrayList<CompiledSourceFile>(parsedSourceFiles.size()); final var compiledSourceFiles = new ArrayList<CompiledSourceFile>(parsedSourceFiles.size());
for (final var parsedSource : parsedSourceFiles) { for (final var parsedSource : parsedSourceFiles) {
if (failedModuleKeys.contains(parsedSource.moduleKey())) { final var blockedModuleKeys = blockedModulesByDependency(failedModuleKeys, moduleDependencyGraph);
if (blockedModuleKeys.contains(parsedSource.moduleKey())) {
continue; continue;
} }
final var compileErrorBaseline = diagnostics.errorCount(); final var compileErrorBaseline = diagnostics.errorCount();
@ -153,8 +155,9 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
compiledSourceFiles.add(new CompiledSourceFile(parsedSource.moduleKey(), irBackendFile)); compiledSourceFiles.add(new CompiledSourceFile(parsedSource.moduleKey(), irBackendFile));
} }
final var blockedModuleKeys = blockedModulesByDependency(failedModuleKeys, moduleDependencyGraph);
for (final var compiledSource : compiledSourceFiles) { for (final var compiledSource : compiledSourceFiles) {
if (failedModuleKeys.contains(compiledSource.moduleKey())) { if (blockedModuleKeys.contains(compiledSource.moduleKey())) {
continue; continue;
} }
irBackendAggregator.merge(compiledSource.irBackendFile()); irBackendAggregator.merge(compiledSource.irBackendFile());
@ -306,10 +309,57 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
} }
} }
private Map<String, Set<String>> buildModuleDependencyGraph(
final ArrayList<ParsedSourceFile> parsedSourceFiles) {
final Map<String, Set<String>> 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<String> blockedModulesByDependency(
final Set<String> failedModuleKeys,
final Map<String, Set<String>> dependenciesByModule) {
final Map<String, Set<String>> 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<String>(failedModuleKeys);
final var pending = new ArrayDeque<String>(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) { private String moduleKey(final PbsModuleVisibilityValidator.ModuleCoordinates coordinates) {
return coordinates.project() + ":" + String.join("/", coordinates.pathSegments().asList()); return coordinates.project() + ":" + String.join("/", coordinates.pathSegments().asList());
} }
private String moduleKey(
final String project,
final ReadOnlyList<String> pathSegments) {
return project + ":" + String.join("/", pathSegments.asList());
}
private static final class MutableModuleUnit { private static final class MutableModuleUnit {
private final ArrayList<PbsModuleVisibilityValidator.SourceFile> sources = new ArrayList<>(); private final ArrayList<PbsModuleVisibilityValidator.SourceFile> sources = new ArrayList<>();
private final ArrayList<PbsModuleVisibilityValidator.BarrelFile> barrels = new ArrayList<>(); private final ArrayList<PbsModuleVisibilityValidator.BarrelFile> barrels = new ArrayList<>();

View File

@ -8,6 +8,7 @@ import p.studio.compiler.models.BuildStack;
import p.studio.compiler.models.ProjectDescriptor; import p.studio.compiler.models.ProjectDescriptor;
import p.studio.compiler.models.SourceHandle; import p.studio.compiler.models.SourceHandle;
import p.studio.compiler.models.SourceKind; 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.InterfaceModuleLoader;
import p.studio.compiler.pbs.stdlib.StdlibEnvironment; import p.studio.compiler.pbs.stdlib.StdlibEnvironment;
import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver; import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
@ -294,7 +295,7 @@ class PBSFrontendPhaseServiceTest {
final var sourceFile = modulePath.resolve("source.pbs"); final var sourceFile = modulePath.resolve("source.pbs");
final var modBarrel = modulePath.resolve("mod.barrel"); final var modBarrel = modulePath.resolve("mod.barrel");
Files.writeString(sourceFile, """ Files.writeString(sourceFile, """
import { draw } from @sdk:gfx; import { Gfx } from @sdk:gfx;
fn use() -> int { return 1; } fn use() -> int { return 1; }
"""); """);
Files.writeString(modBarrel, "pub fn use() -> int;"); Files.writeString(modBarrel, "pub fn use() -> int;");
@ -314,8 +315,16 @@ class PBSFrontendPhaseServiceTest {
final var stdlibModule = new StdlibModuleSource( final var stdlibModule = new StdlibModuleSource(
"sdk", "sdk",
ReadOnlyList.wrap(List.of("gfx")), ReadOnlyList.wrap(List.of("gfx")),
ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile("gfx.pbs", "fn draw() -> int { return 1; }"))), ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile(
"pub fn draw() -> int;"); "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( final var frontendService = new PBSFrontendPhaseService(
resolverForMajor(7, stdlibModule), resolverForMajor(7, stdlibModule),
new InterfaceModuleLoader(2_000_000)); new InterfaceModuleLoader(2_000_000));
@ -621,8 +630,157 @@ class PBSFrontendPhaseServiceTest {
.orElseThrow(); .orElseThrow();
assertEquals(p.studio.compiler.source.diagnostics.DiagnosticPhase.STATIC_SEMANTICS, semanticsDiagnostic.getPhase()); assertEquals(p.studio.compiler.source.diagnostics.DiagnosticPhase.STATIC_SEMANTICS, semanticsDiagnostic.getPhase());
assertEquals(semanticsDiagnostic.getCode(), semanticsDiagnostic.getTemplateId()); 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(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( private void registerFile(