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,
|
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.
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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<>();
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user