implements PR031
This commit is contained in:
parent
495104db6d
commit
f7abaf27d4
@ -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.
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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<>();
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user