implements PR030
This commit is contained in:
parent
01c5b6649a
commit
495104db6d
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -181,6 +181,7 @@ Compiler:
|
||||
- compiles source to PBX,
|
||||
- emits `SYSC`,
|
||||
- emits `HOSTCALL <sysc_index>`,
|
||||
- computes deterministic `requiredCapabilities` from admitted host bindings for packer tooling,
|
||||
- does not grant authority.
|
||||
|
||||
Packer:
|
||||
|
||||
@ -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<IRFunction> functions = sourceKind == SourceKind.SDK_INTERFACE
|
||||
? ReadOnlyList.empty()
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -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<String> 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<String>();
|
||||
final var firstCapabilityByHostBinding = new HashMap<String, FirstBindingCapability>();
|
||||
|
||||
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<String> normalizedSet(final ReadOnlyList<String> values) {
|
||||
final var normalized = new HashSet<String>();
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@ -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<String> requiredCapabilities(
|
||||
final List<IRReservedMetadata.HostMethodBinding> hostMethodBindings) {
|
||||
final var required = new LinkedHashSet<String>();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> 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(
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = """
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
""",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
"""))),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> knownCapabilities,
|
||||
ReadOnlyList<String> declaredCapabilities,
|
||||
boolean enforceDeclaredCapabilities) {
|
||||
|
||||
private static final ReadOnlyList<String> 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<String> declaredCapabilities) {
|
||||
return new HostAdmissionContext(DEFAULT_KNOWN_CAPABILITIES, declaredCapabilities, true);
|
||||
}
|
||||
|
||||
private static ReadOnlyList<String> normalize(final ReadOnlyList<String> values) {
|
||||
final Set<String> 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());
|
||||
}
|
||||
}
|
||||
@ -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<IRReservedMetadata.HostMethodBinding> hostMethodBindings = new ArrayList<>();
|
||||
private final ArrayList<IRReservedMetadata.BuiltinTypeSurface> builtinTypeSurfaces = new ArrayList<>();
|
||||
private final ArrayList<IRReservedMetadata.BuiltinConstSurface> builtinConstSurfaces = new ArrayList<>();
|
||||
private final LinkedHashSet<String> 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()) {
|
||||
|
||||
@ -8,16 +8,22 @@ import java.util.Objects;
|
||||
public record IRReservedMetadata(
|
||||
ReadOnlyList<HostMethodBinding> hostMethodBindings,
|
||||
ReadOnlyList<BuiltinTypeSurface> builtinTypeSurfaces,
|
||||
ReadOnlyList<BuiltinConstSurface> builtinConstSurfaces) {
|
||||
ReadOnlyList<BuiltinConstSurface> builtinConstSurfaces,
|
||||
ReadOnlyList<String> 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");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user