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,
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.

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.
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).

View File

@ -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<CompiledSourceFile>(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<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) {
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 final ArrayList<PbsModuleVisibilityValidator.SourceFile> sources = 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.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(