diff --git a/docs/pbs/specs/12. Diagnostics Specification.md b/docs/pbs/specs/12. Diagnostics Specification.md index ac833182..a11ecedc 100644 --- a/docs/pbs/specs/12. Diagnostics Specification.md +++ b/docs/pbs/specs/12. Diagnostics Specification.md @@ -179,6 +179,8 @@ At minimum, the PBS diagnostics baseline must cover: 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. +At minimum, host-admission diagnostics must cover missing or malformed host capability metadata and unknown or undeclared capability names. + Only backend-originated failures that remain source-attributable and user-actionable belong to the PBS-facing diagnostics contract. This means: diff --git a/docs/pbs/specs/13. Lowering IRBackend Specification.md b/docs/pbs/specs/13. Lowering IRBackend Specification.md index 5ec8c008..72ed2dac 100644 --- a/docs/pbs/specs/13. Lowering IRBackend Specification.md +++ b/docs/pbs/specs/13. Lowering IRBackend Specification.md @@ -75,6 +75,7 @@ For each admitted source unit and callable in the current lowering slice, `IRBac 3. declared return surface information, 4. source attribution anchor (`file + span`) for diagnostics and traceability, 5. source-observable parse intent for statement/expression structure (including precedence/associativity outcome already fixed by AST shape). +6. deterministic `requiredCapabilities` derived from admitted host-binding metadata for packer/runtime-manifest assistance. Lowering must not collapse source categories in a way that erases required declaration/callable identity needed by downstream diagnostics or conformance assertions. diff --git a/docs/pbs/specs/4. Static Semantics Specification.md b/docs/pbs/specs/4. Static Semantics Specification.md index 0c51aa65..8253593b 100644 --- a/docs/pbs/specs/4. Static Semantics Specification.md +++ b/docs/pbs/specs/4. Static Semantics Specification.md @@ -62,8 +62,9 @@ Rules: - Attributes are not first-class values and are not reflectable in v1 core. - Attributes do not automatically survive into runtime or bytecode artifacts. - An attribute affects runtime artifacts only when another specification defines an explicit lowering for its semantic effect. -- In v1 core, the normative reserved attributes are `Host`, `BuiltinType`, `BuiltinConst`, and `IntrinsicCall`. +- In v1 core, the normative reserved attributes are `Host`, `Capability`, `BuiltinType`, `BuiltinConst`, and `IntrinsicCall`. - `Host` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body. +- `Capability` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body. - `Host` is invalid on ordinary user-authored modules, top-level `fn`, struct methods, service methods, callbacks, contracts, and constants. - `Host` metadata is consumed by the compiler during host-binding lowering. - The `Host` attribute syntax itself is not exported as runtime metadata; instead, its canonical identity participates in PBX host-binding emission as defined by the Host ABI Binding specification. @@ -168,9 +169,12 @@ Rules: - Any payload-less `optional` type surface is statically invalid. - Any `optional void` type surface is statically invalid. - A host method signature in a reserved stdlib/interface module MUST carry exactly one `Host` attribute. +- A host method signature in a reserved stdlib/interface module MUST carry exactly one `Capability` attribute. - A `Host` attribute MUST declare exactly the named arguments `module`, `name`, and `version`. - `Host.module` and `Host.name` MUST be non-empty string literals. - `Host.version` MUST be a positive integer literal. +- A `Capability` attribute MUST declare exactly the named argument `name`. +- `Capability.name` MUST be a non-empty lowercase capability identifier. - Two host methods in the same resolved stdlib environment MUST NOT lower to the same canonical `(module, name, version)` unless they denote the same host method declaration after project/module resolution. - A `declare const` declaration must include an explicit type annotation. - A `declare const` declaration without `BuiltinConst` metadata must include an initializer. @@ -655,6 +659,7 @@ At minimum, deterministic static diagnostics are required for: - invalid host method declaration shape, - invalid attribute surface, - invalid `Host` attribute target, +- invalid `Capability` attribute target, - invalid `BuiltinType` attribute target, - invalid `BuiltinConst` attribute target, - invalid `IntrinsicCall` attribute target, @@ -663,6 +668,8 @@ At minimum, deterministic static diagnostics are required for: - malformed `Host(module=..., name=..., version=...)` argument set, - invalid empty `Host.module` or `Host.name`, - invalid non-positive `Host.version`, +- malformed `Capability(name=...)` argument set, +- invalid empty or non-canonical `Capability.name`, - invalid builtin declaration shape, - invalid builtin method declaration shape, - missing required `BuiltinType` attribute on a reserved builtin declaration, diff --git a/docs/pbs/specs/6.2. Host ABI Binding and Loader Resolution Specification.md b/docs/pbs/specs/6.2. Host ABI Binding and Loader Resolution Specification.md index cfddf1e6..a1e88813 100644 --- a/docs/pbs/specs/6.2. Host ABI Binding and Loader Resolution Specification.md +++ b/docs/pbs/specs/6.2. Host ABI Binding and Loader Resolution Specification.md @@ -118,6 +118,7 @@ The source-level call may originate from an SDK declaration such as: ```pbs declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int, c: color); } ``` @@ -287,6 +288,7 @@ Capability gating is mandatory during load. Rules: - every resolved host binding declares required capabilities, +- frontend/build host-admission computes deterministic `requiredCapabilities` from host binding metadata for packer assistance, - the cartridge manifest declares requested capabilities using a nominal capability list, - the loader derives or receives the granted capability set from the cartridge/platform policy environment, - if a required capability is not granted, load fails, diff --git a/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md b/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md index 94828328..50736d2b 100644 --- a/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md +++ b/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md @@ -181,6 +181,7 @@ Compiler: - compiles source to PBX, - emits `SYSC`, - emits `HOSTCALL `, +- computes deterministic `requiredCapabilities` from admitted host bindings for packer tooling, - does not grant authority. Packer: diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java index b462bedd..78687576 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java @@ -3,6 +3,8 @@ package p.studio.compiler.pbs; import p.studio.compiler.models.IRFunction; import p.studio.compiler.models.SourceKind; import p.studio.compiler.models.IRBackendFile; +import p.studio.compiler.messages.HostAdmissionContext; +import p.studio.compiler.models.IRReservedMetadata; import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.lexer.PbsLexer; import p.studio.compiler.pbs.metadata.PbsReservedMetadataExtractor; @@ -19,6 +21,7 @@ public final class PbsFrontendCompiler { private final PbsDeclarationSemanticsValidator declarationSemanticsValidator = new PbsDeclarationSemanticsValidator(); private final PbsFlowSemanticsValidator flowSemanticsValidator = new PbsFlowSemanticsValidator(); private final PbsReservedMetadataExtractor reservedMetadataExtractor = new PbsReservedMetadataExtractor(); + private final PbsHostAdmissionValidator hostAdmissionValidator = new PbsHostAdmissionValidator(); public IRBackendFile compileFile( final FileId fileId, @@ -32,13 +35,22 @@ public final class PbsFrontendCompiler { final String source, final DiagnosticSink diagnostics, final SourceKind sourceKind) { + return compileFile(fileId, source, diagnostics, sourceKind, HostAdmissionContext.permissiveDefault()); + } + + public IRBackendFile compileFile( + final FileId fileId, + final String source, + final DiagnosticSink diagnostics, + final SourceKind sourceKind, + final HostAdmissionContext hostAdmissionContext) { final var admissionBaseline = diagnostics.errorCount(); final var tokens = PbsLexer.lex(source, fileId, diagnostics); final var parseMode = sourceKind == SourceKind.SDK_INTERFACE ? PbsParser.ParseMode.INTERFACE_MODULE : PbsParser.ParseMode.ORDINARY; final var ast = PbsParser.parse(tokens, fileId, diagnostics, parseMode); - final var irBackendFile = compileParsedFile(fileId, ast, diagnostics, sourceKind); + final var irBackendFile = compileParsedFile(fileId, ast, diagnostics, sourceKind, hostAdmissionContext); if (diagnostics.errorCount() > admissionBaseline) { return IRBackendFile.empty(fileId); } @@ -57,6 +69,15 @@ public final class PbsFrontendCompiler { final PbsAst.File ast, final DiagnosticSink diagnostics, final SourceKind sourceKind) { + return compileParsedFile(fileId, ast, diagnostics, sourceKind, HostAdmissionContext.permissiveDefault()); + } + + public IRBackendFile compileParsedFile( + final FileId fileId, + final PbsAst.File ast, + final DiagnosticSink diagnostics, + final SourceKind sourceKind, + final HostAdmissionContext hostAdmissionContext) { final var semanticsErrorBaseline = diagnostics.errorCount(); declarationSemanticsValidator.validate(ast, sourceKind, diagnostics); flowSemanticsValidator.validate(ast, diagnostics); @@ -64,7 +85,20 @@ public final class PbsFrontendCompiler { return IRBackendFile.empty(fileId); } - final var reservedMetadata = reservedMetadataExtractor.extract(ast, sourceKind); + final var extractedReservedMetadata = reservedMetadataExtractor.extract(ast, sourceKind); + final var hostAdmissionErrorBaseline = diagnostics.errorCount(); + final var requiredCapabilities = hostAdmissionValidator.validate( + extractedReservedMetadata, + hostAdmissionContext, + diagnostics); + if (diagnostics.errorCount() > hostAdmissionErrorBaseline) { + return IRBackendFile.empty(fileId); + } + final var reservedMetadata = new IRReservedMetadata( + extractedReservedMetadata.hostMethodBindings(), + extractedReservedMetadata.builtinTypeSurfaces(), + extractedReservedMetadata.builtinConstSurfaces(), + requiredCapabilities); final ReadOnlyList functions = sourceKind == SourceKind.SDK_INTERFACE ? ReadOnlyList.empty() diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsHostAdmissionErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsHostAdmissionErrors.java new file mode 100644 index 00000000..d201f5db --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsHostAdmissionErrors.java @@ -0,0 +1,9 @@ +package p.studio.compiler.pbs; + +public enum PbsHostAdmissionErrors { + E_HOST_MISSING_REQUIRED_CAPABILITY, + E_HOST_MALFORMED_CAPABILITY_METADATA, + E_HOST_UNKNOWN_CAPABILITY, + E_HOST_CAPABILITY_NOT_DECLARED, + E_HOST_INCONSISTENT_BINDING_CAPABILITY, +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsHostAdmissionValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsHostAdmissionValidator.java new file mode 100644 index 00000000..35a78daa --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsHostAdmissionValidator.java @@ -0,0 +1,129 @@ +package p.studio.compiler.pbs; + +import p.studio.compiler.messages.HostAdmissionContext; +import p.studio.compiler.models.IRReservedMetadata; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.diagnostics.Diagnostics; +import p.studio.compiler.source.diagnostics.RelatedSpan; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +public final class PbsHostAdmissionValidator { + private static final Pattern CAPABILITY_NAME = Pattern.compile("^[a-z][a-z0-9_]*$"); + + public ReadOnlyList validate( + final IRReservedMetadata metadata, + final HostAdmissionContext context, + final DiagnosticSink diagnostics) { + final var knownCapabilities = normalizedSet(context.knownCapabilities()); + final var declaredCapabilities = normalizedSet(context.declaredCapabilities()); + final var requiredCapabilities = new LinkedHashSet(); + final var firstCapabilityByHostBinding = new HashMap(); + + for (final var hostBinding : metadata.hostMethodBindings()) { + final var normalizedCapability = normalize(hostBinding.requiredCapability()); + if (!hostBinding.capabilityDeclared()) { + Diagnostics.error( + diagnostics, + PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name(), + "Host binding '%s.%s' requires Capability(name=...) metadata" + .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName()), + hostBinding.span()); + continue; + } + if (normalizedCapability.isBlank()) { + Diagnostics.error( + diagnostics, + PbsHostAdmissionErrors.E_HOST_MALFORMED_CAPABILITY_METADATA.name(), + "Host binding '%s.%s' has malformed Capability(name=...) metadata" + .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName()), + hostBinding.span()); + continue; + } + + if (!CAPABILITY_NAME.matcher(normalizedCapability).matches()) { + Diagnostics.error( + diagnostics, + PbsHostAdmissionErrors.E_HOST_MALFORMED_CAPABILITY_METADATA.name(), + "Host binding '%s.%s' has malformed capability name '%s'" + .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName(), normalizedCapability), + hostBinding.span()); + continue; + } + + if (!knownCapabilities.contains(normalizedCapability)) { + Diagnostics.error( + diagnostics, + PbsHostAdmissionErrors.E_HOST_UNKNOWN_CAPABILITY.name(), + "Host binding '%s.%s' references unknown capability '%s'" + .formatted(hostBinding.ownerName(), hostBinding.sourceMethodName(), normalizedCapability), + hostBinding.span()); + continue; + } + + final var bindingKey = "%s|%s|%d".formatted( + hostBinding.abiModule(), + hostBinding.abiMethod(), + hostBinding.abiVersion()); + final var firstBindingCapability = firstCapabilityByHostBinding.putIfAbsent( + bindingKey, + new FirstBindingCapability(normalizedCapability, hostBinding.span())); + if (firstBindingCapability != null && !firstBindingCapability.capability().equals(normalizedCapability)) { + Diagnostics.error( + diagnostics, + PbsHostAdmissionErrors.E_HOST_INCONSISTENT_BINDING_CAPABILITY.name(), + "Canonical host binding (%s, %s, %d) has inconsistent capability metadata ('%s' vs '%s')" + .formatted( + hostBinding.abiModule(), + hostBinding.abiMethod(), + hostBinding.abiVersion(), + firstBindingCapability.capability(), + normalizedCapability), + hostBinding.span(), + List.of(new RelatedSpan("First capability declaration for this binding is here", firstBindingCapability.span()))); + continue; + } + + if (context.enforceDeclaredCapabilities() && !declaredCapabilities.contains(normalizedCapability)) { + Diagnostics.error( + diagnostics, + PbsHostAdmissionErrors.E_HOST_CAPABILITY_NOT_DECLARED.name(), + "Capability '%s' required by host binding '%s.%s' is not declared in host admission context" + .formatted(normalizedCapability, hostBinding.ownerName(), hostBinding.sourceMethodName()), + hostBinding.span()); + continue; + } + + requiredCapabilities.add(normalizedCapability); + } + + return ReadOnlyList.wrap(requiredCapabilities.stream().toList()); + } + + private Set normalizedSet(final ReadOnlyList values) { + final var normalized = new HashSet(); + for (final var value : values) { + final var capability = normalize(value); + if (!capability.isBlank()) { + normalized.add(capability); + } + } + return normalized; + } + + private String normalize(final String capability) { + return capability == null ? "" : capability.trim().toLowerCase(Locale.ROOT); + } + + private record FirstBindingCapability( + String capability, + p.studio.compiler.source.Span span) { + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java index ffda815b..0771a6cc 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java @@ -7,6 +7,7 @@ import p.studio.compiler.pbs.semantics.PbsBuiltinLayoutResolver; import p.studio.utilities.structures.ReadOnlyList; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -15,6 +16,7 @@ import java.util.stream.Collectors; public final class PbsReservedMetadataExtractor { private static final String ATTR_HOST = "Host"; + private static final String ATTR_CAPABILITY = "Capability"; private static final String ATTR_BUILTIN_TYPE = "BuiltinType"; private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall"; private static final String ATTR_BUILTIN_CONST = "BuiltinConst"; @@ -51,7 +53,8 @@ public final class PbsReservedMetadataExtractor { return new IRReservedMetadata( ReadOnlyList.wrap(hostMethodBindings), ReadOnlyList.wrap(builtinTypeSurfaces), - ReadOnlyList.wrap(builtinConstSurfaces)); + ReadOnlyList.wrap(builtinConstSurfaces), + requiredCapabilities(hostMethodBindings)); } private void extractHostBindings( @@ -66,12 +69,18 @@ public final class PbsReservedMetadataExtractor { final var abiModule = stringArgument(hostMetadata, "module").orElse(""); final var abiMethod = stringArgument(hostMetadata, "name").orElse(signature.name()); final var abiVersion = longArgument(hostMetadata, "version").orElse(0L); + final var capabilityAttribute = firstAttributeNamed(signature.attributes(), ATTR_CAPABILITY); + final var capability = capabilityAttribute + .flatMap(attr -> stringArgument(attr, "name")) + .orElse(""); hostMethodBindings.add(new IRReservedMetadata.HostMethodBinding( hostDecl.name(), signature.name(), abiModule, abiMethod, abiVersion, + capabilityAttribute.isPresent(), + capability, signature.span())); } } @@ -218,4 +227,16 @@ public final class PbsReservedMetadataExtractor { case ERROR -> "error"; }; } + + private ReadOnlyList requiredCapabilities( + final List hostMethodBindings) { + final var required = new LinkedHashSet(); + for (final var hostBinding : hostMethodBindings) { + if (hostBinding.requiredCapability() == null || hostBinding.requiredCapability().isBlank()) { + continue; + } + required.add(hostBinding.requiredCapability().trim().toLowerCase()); + } + return ReadOnlyList.wrap(required.stream().toList()); + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java index 9e301640..88e7f39f 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java @@ -16,12 +16,14 @@ import java.util.Set; public final class PbsDeclarationSemanticsValidator { private static final String ATTR_HOST = "Host"; + private static final String ATTR_CAPABILITY = "Capability"; private static final String ATTR_BUILTIN_TYPE = "BuiltinType"; private static final String ATTR_BUILTIN_CONST = "BuiltinConst"; private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall"; private static final Set RESERVED_ATTRIBUTES = Set.of( ATTR_HOST, + ATTR_CAPABILITY, ATTR_BUILTIN_TYPE, ATTR_BUILTIN_CONST, ATTR_INTRINSIC_CALL); @@ -386,7 +388,7 @@ public final class PbsDeclarationSemanticsValidator { if (!isReservedAttribute(attribute.name())) { continue; } - if (ATTR_HOST.equals(attribute.name())) { + if (ATTR_HOST.equals(attribute.name()) || ATTR_CAPABILITY.equals(attribute.name())) { continue; } reportInvalidReservedAttributeTarget( 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 724d38ee..17a8cd7a 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 @@ -145,7 +145,8 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { parsedSource.fileId(), parsedSource.ast(), diagnostics, - parsedSource.sourceKind()); + parsedSource.sourceKind(), + ctx.hostAdmissionContext()); if (diagnostics.errorCount() > compileErrorBaseline) { failedModuleKeys.add(parsedSource.moduleKey()); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/gfx/main.pbs b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/gfx/main.pbs index cd0c713f..eb8dc359 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/gfx/main.pbs +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/gfx/main.pbs @@ -2,5 +2,6 @@ import { Color } from @core:color; declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int, color: Color) -> void; } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java index ea7ab3c9..a3f645af 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsDiagnosticsContractTest.java @@ -103,4 +103,29 @@ class PbsDiagnosticsContractTest { assertEquals(diagnostic.getCode(), diagnostic.getTemplateId()); assertEquals(9, diagnostic.getSpan().getFileId().getId()); } + + @Test + void shouldTagHostAdmissionDiagnosticsWithHostAdmissionPhase() { + final var source = """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int) -> void; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile( + new FileId(10), + source, + diagnostics, + p.studio.compiler.models.SourceKind.SDK_INTERFACE); + + final var diagnostic = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name())) + .findFirst() + .orElseThrow(); + assertEquals(DiagnosticPhase.HOST_ADMISSION, diagnostic.getPhase()); + assertEquals(diagnostic.getCode(), diagnostic.getTemplateId()); + assertEquals(10, diagnostic.getSpan().getFileId().getId()); + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java index c307cb2e..f30a4a6c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java @@ -1,11 +1,14 @@ package p.studio.compiler.pbs; import org.junit.jupiter.api.Test; +import p.studio.compiler.messages.HostAdmissionContext; import p.studio.compiler.models.SourceKind; import p.studio.compiler.pbs.lexer.LexErrors; import p.studio.compiler.pbs.semantics.PbsSemanticsErrors; +import p.studio.compiler.source.diagnostics.DiagnosticPhase; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -115,6 +118,7 @@ class PbsFrontendCompilerTest { } declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int, color: Color) -> void; } [BuiltinConst(target = "Color", name = "white", version = 1)] @@ -130,10 +134,102 @@ class PbsFrontendCompilerTest { assertEquals(1, fileBackend.reservedMetadata().hostMethodBindings().size()); assertEquals(1, fileBackend.reservedMetadata().builtinTypeSurfaces().size()); assertEquals(1, fileBackend.reservedMetadata().builtinConstSurfaces().size()); + assertEquals(1, fileBackend.reservedMetadata().requiredCapabilities().size()); + assertEquals("gfx", fileBackend.reservedMetadata().requiredCapabilities().getFirst()); assertEquals("Color", fileBackend.reservedMetadata().builtinTypeSurfaces().getFirst().canonicalTypeName()); assertEquals(3, fileBackend.reservedMetadata().builtinTypeSurfaces().getFirst().fields().size()); } + @Test + void shouldRejectHostBindingWithoutCapabilityAtHostAdmission() { + final var source = """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int) -> void; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var compiler = new PbsFrontendCompiler(); + final var fileBackend = compiler.compileFile(new FileId(7), source, diagnostics, SourceKind.SDK_INTERFACE); + + final var hostDiagnostic = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name())) + .findFirst() + .orElseThrow(); + assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase()); + assertEquals(hostDiagnostic.getCode(), hostDiagnostic.getTemplateId()); + assertEquals(0, fileBackend.functions().size()); + } + + @Test + void shouldRejectHostBindingCapabilityNotDeclaredInStrictContext() { + final var source = """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] + fn draw_pixel(x: int, y: int) -> void; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var compiler = new PbsFrontendCompiler(); + compiler.compileFile( + new FileId(8), + source, + diagnostics, + SourceKind.SDK_INTERFACE, + HostAdmissionContext.strictWithDeclaredCapabilities(ReadOnlyList.wrap(java.util.List.of("input")))); + + final var hostDiagnostic = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_CAPABILITY_NOT_DECLARED.name())) + .findFirst() + .orElseThrow(); + assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase()); + } + + @Test + void shouldRejectUnknownHostCapabilityAtHostAdmission() { + final var source = """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "video")] + fn draw_pixel(x: int, y: int) -> void; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var compiler = new PbsFrontendCompiler(); + compiler.compileFile(new FileId(9), source, diagnostics, SourceKind.SDK_INTERFACE); + + final var hostDiagnostic = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_UNKNOWN_CAPABILITY.name())) + .findFirst() + .orElseThrow(); + assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase()); + } + + @Test + void shouldRejectMalformedHostCapabilityMetadataAtHostAdmission() { + final var source = """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(version = 1)] + fn draw_pixel(x: int, y: int) -> void; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var compiler = new PbsFrontendCompiler(); + compiler.compileFile(new FileId(10), source, diagnostics, SourceKind.SDK_INTERFACE); + + final var hostDiagnostic = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MALFORMED_CAPABILITY_METADATA.name())) + .findFirst() + .orElseThrow(); + assertEquals(DiagnosticPhase.HOST_ADMISSION, hostDiagnostic.getPhase()); + } + @Test void shouldInferBuiltinFieldSlotsFromDeclarationOrder() { final var source = """ diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java index 44deb1b2..d0cc3ae4 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java @@ -59,6 +59,7 @@ class PbsGateUSdkInterfaceConformanceTest { declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int, color: Color) -> void; } """; @@ -165,6 +166,7 @@ class PbsGateUSdkInterfaceConformanceTest { declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int, color: Color) -> void; } """; @@ -229,6 +231,7 @@ class PbsGateUSdkInterfaceConformanceTest { declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int, color: Color) -> void; } @@ -268,6 +271,30 @@ class PbsGateUSdkInterfaceConformanceTest { DiagnosticPhase.STATIC_SEMANTICS); } + @Test + void gateU_shouldEmitHostAdmissionDiagnosticForMissingCapabilityMetadata() { + final var source = """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int) -> void; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile( + new FileId(42), + source, + diagnostics, + SourceKind.SDK_INTERFACE); + + final var hostAdmission = firstDiagnostic(diagnostics, + d -> d.getCode().equals(PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name())); + assertStableDiagnosticIdentity( + hostAdmission, + PbsHostAdmissionErrors.E_HOST_MISSING_REQUIRED_CAPABILITY.name(), + DiagnosticPhase.HOST_ADMISSION); + } + private WorkspaceCompileResult compileWorkspaceModule( final Path projectRoot, final String sourceContent, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java index 9cc80662..96c682c2 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java @@ -224,6 +224,7 @@ class PbsModuleVisibilityTest { """ declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int) -> void; } """, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java index 10647f48..a2119573 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java @@ -127,6 +127,7 @@ class PbsParserTest { void shouldRejectAttributesInOrdinarySourceAndRecover() { final var source = """ [Host(module = "gfx", name = "draw", version = 1)] + [Capability(name = "gfx")] fn run() -> int { return 1; } declare struct Point(x: int,); """; @@ -230,6 +231,7 @@ class PbsParserTest { } declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw(x: int, y: int, color: Color) -> void; } [BuiltinConst(target = "Color", name = "white", version = 1)] @@ -256,7 +258,7 @@ class PbsParserTest { final var hostDecl = assertInstanceOf(PbsAst.HostDecl.class, ast.topDecls().get(1)); assertEquals(1, hostDecl.signatures().size()); - assertEquals(1, hostDecl.signatures().getFirst().attributes().size()); + assertEquals(2, hostDecl.signatures().getFirst().attributes().size()); final var constDecl = assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(2)); assertEquals(1, constDecl.attributes().size()); @@ -267,6 +269,7 @@ class PbsParserTest { final var source = """ declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw(x: int, y: int) -> void; unexpected_token } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java index cc44a32f..889db1f4 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java @@ -24,6 +24,7 @@ class PbsInterfaceModuleSemanticsTest { declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int, c: Color) -> void; } @@ -51,6 +52,7 @@ class PbsInterfaceModuleSemanticsTest { declare contract Api { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn run() -> void; } 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 9f7c7e63..135760fc 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 @@ -246,6 +246,7 @@ class PBSFrontendPhaseServiceTest { } declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw(x: int, y: int, color: Color) -> void; } """); @@ -529,6 +530,7 @@ class PBSFrontendPhaseServiceTest { """ declare host Gfx { [Host(module = "gfx", name = "draw_pixel", version = 1)] + [Capability(name = "gfx")] fn draw_pixel(x: int, y: int) -> void; } """))), diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/messages/FrontendPhaseContext.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/messages/FrontendPhaseContext.java index 7c4e9c62..42612633 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/messages/FrontendPhaseContext.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/messages/FrontendPhaseContext.java @@ -11,12 +11,13 @@ public class FrontendPhaseContext { public final FileTableReader fileTable; public final BuildStack stack; private final int stdlibVersion; + private final HostAdmissionContext hostAdmissionContext; public FrontendPhaseContext( final ProjectTableReader projectTable, final FileTableReader fileTable, final BuildStack stack) { - this(projectTable, fileTable, stack, 1); + this(projectTable, fileTable, stack, 1, HostAdmissionContext.permissiveDefault()); } public FrontendPhaseContext( @@ -24,10 +25,22 @@ public class FrontendPhaseContext { final FileTableReader fileTable, final BuildStack stack, final int stdlibVersion) { + this(projectTable, fileTable, stack, stdlibVersion, HostAdmissionContext.permissiveDefault()); + } + + public FrontendPhaseContext( + final ProjectTableReader projectTable, + final FileTableReader fileTable, + final BuildStack stack, + final int stdlibVersion, + final HostAdmissionContext hostAdmissionContext) { this.projectTable = projectTable; this.fileTable = fileTable; this.stack = stack; this.stdlibVersion = stdlibVersion; + this.hostAdmissionContext = hostAdmissionContext == null + ? HostAdmissionContext.permissiveDefault() + : hostAdmissionContext; } public SourceKind sourceKind(final ProjectId projectId) { @@ -37,4 +50,8 @@ public class FrontendPhaseContext { public int stdlibVersion() { return stdlibVersion; } + + public HostAdmissionContext hostAdmissionContext() { + return hostAdmissionContext; + } } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/messages/HostAdmissionContext.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/messages/HostAdmissionContext.java new file mode 100644 index 00000000..b4055db8 --- /dev/null +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/messages/HostAdmissionContext.java @@ -0,0 +1,53 @@ +package p.studio.compiler.messages; + +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public record HostAdmissionContext( + ReadOnlyList knownCapabilities, + ReadOnlyList declaredCapabilities, + boolean enforceDeclaredCapabilities) { + + private static final ReadOnlyList DEFAULT_KNOWN_CAPABILITIES = ReadOnlyList.wrap(List.of( + "system", + "gfx", + "input", + "audio", + "fs", + "log", + "asset", + "bank")); + + public HostAdmissionContext { + knownCapabilities = knownCapabilities == null || knownCapabilities.isEmpty() + ? DEFAULT_KNOWN_CAPABILITIES + : normalize(knownCapabilities); + declaredCapabilities = declaredCapabilities == null + ? ReadOnlyList.empty() + : normalize(declaredCapabilities); + } + + public static HostAdmissionContext permissiveDefault() { + return new HostAdmissionContext(DEFAULT_KNOWN_CAPABILITIES, ReadOnlyList.empty(), false); + } + + public static HostAdmissionContext strictWithDeclaredCapabilities( + final ReadOnlyList declaredCapabilities) { + return new HostAdmissionContext(DEFAULT_KNOWN_CAPABILITIES, declaredCapabilities, true); + } + + private static ReadOnlyList normalize(final ReadOnlyList values) { + final Set dedup = new LinkedHashSet<>(); + for (final var value : values) { + if (value == null || value.isBlank()) { + continue; + } + dedup.add(value.trim().toLowerCase(Locale.ROOT)); + } + return ReadOnlyList.wrap(dedup.stream().toList()); + } +} diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java index af91d7f4..c7c237b7 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java @@ -5,6 +5,7 @@ import lombok.Getter; import p.studio.utilities.structures.ReadOnlyList; import java.util.ArrayList; +import java.util.LinkedHashSet; @Builder @Getter @@ -23,6 +24,7 @@ public class IRBackend { private final ArrayList hostMethodBindings = new ArrayList<>(); private final ArrayList builtinTypeSurfaces = new ArrayList<>(); private final ArrayList builtinConstSurfaces = new ArrayList<>(); + private final LinkedHashSet requiredCapabilities = new LinkedHashSet<>(); public void merge(final IRBackendFile backendFile) { if (backendFile == null) { @@ -33,6 +35,7 @@ public class IRBackend { hostMethodBindings.addAll(metadata.hostMethodBindings().asList()); builtinTypeSurfaces.addAll(metadata.builtinTypeSurfaces().asList()); builtinConstSurfaces.addAll(metadata.builtinConstSurfaces().asList()); + requiredCapabilities.addAll(metadata.requiredCapabilities().asList()); } public IRBackend emit() { @@ -42,7 +45,8 @@ public class IRBackend { .reservedMetadata(new IRReservedMetadata( ReadOnlyList.wrap(hostMethodBindings), ReadOnlyList.wrap(builtinTypeSurfaces), - ReadOnlyList.wrap(builtinConstSurfaces))) + ReadOnlyList.wrap(builtinConstSurfaces), + ReadOnlyList.wrap(requiredCapabilities.stream().toList()))) .build(); } } @@ -54,6 +58,7 @@ public class IRBackend { .append(", hostBindings=").append(reservedMetadata.hostMethodBindings().size()) .append(", builtinTypes=").append(reservedMetadata.builtinTypeSurfaces().size()) .append(", builtinConsts=").append(reservedMetadata.builtinConstSurfaces().size()) + .append(", requiredCapabilities=").append(reservedMetadata.requiredCapabilities().size()) .append('}'); if (functions.isEmpty()) { diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRReservedMetadata.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRReservedMetadata.java index 6bb251ae..b5ed1a5b 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRReservedMetadata.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRReservedMetadata.java @@ -8,16 +8,22 @@ import java.util.Objects; public record IRReservedMetadata( ReadOnlyList hostMethodBindings, ReadOnlyList builtinTypeSurfaces, - ReadOnlyList builtinConstSurfaces) { + ReadOnlyList builtinConstSurfaces, + ReadOnlyList requiredCapabilities) { public IRReservedMetadata { hostMethodBindings = hostMethodBindings == null ? ReadOnlyList.empty() : hostMethodBindings; builtinTypeSurfaces = builtinTypeSurfaces == null ? ReadOnlyList.empty() : builtinTypeSurfaces; builtinConstSurfaces = builtinConstSurfaces == null ? ReadOnlyList.empty() : builtinConstSurfaces; + requiredCapabilities = requiredCapabilities == null ? ReadOnlyList.empty() : requiredCapabilities; } public static IRReservedMetadata empty() { - return new IRReservedMetadata(ReadOnlyList.empty(), ReadOnlyList.empty(), ReadOnlyList.empty()); + return new IRReservedMetadata( + ReadOnlyList.empty(), + ReadOnlyList.empty(), + ReadOnlyList.empty(), + ReadOnlyList.empty()); } public record HostMethodBinding( @@ -26,12 +32,15 @@ public record IRReservedMetadata( String abiModule, String abiMethod, long abiVersion, + boolean capabilityDeclared, + String requiredCapability, Span span) { public HostMethodBinding { ownerName = Objects.requireNonNull(ownerName, "ownerName"); sourceMethodName = Objects.requireNonNull(sourceMethodName, "sourceMethodName"); abiModule = Objects.requireNonNull(abiModule, "abiModule"); abiMethod = Objects.requireNonNull(abiMethod, "abiMethod"); + requiredCapability = requiredCapability == null ? "" : requiredCapability; span = Objects.requireNonNull(span, "span"); } }