implements PR-19.6 lifecycle and initallowed semantics

This commit is contained in:
bQUARKz 2026-03-26 19:28:22 +00:00
parent 2003fc749e
commit 7fc38010a2
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
8 changed files with 607 additions and 2 deletions

View File

@ -11,6 +11,7 @@ import p.studio.compiler.pbs.lowering.PbsExecutableLoweringService;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.pbs.semantics.PbsDeclarationSemanticsValidator;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator;
import p.studio.compiler.pbs.semantics.PbsLifecycleSemanticsValidator;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
@ -23,6 +24,7 @@ import java.util.HashSet;
public final class PbsFrontendCompiler {
private final PbsFlowSemanticsValidator flowSemanticsValidator = new PbsFlowSemanticsValidator();
private final PbsLifecycleSemanticsValidator lifecycleSemanticsValidator = new PbsLifecycleSemanticsValidator();
private final PbsReservedMetadataExtractor reservedMetadataExtractor = new PbsReservedMetadataExtractor();
private final PbsHostAdmissionValidator hostAdmissionValidator = new PbsHostAdmissionValidator();
private final PbsExecutableLoweringService executableLoweringService = new PbsExecutableLoweringService();
@ -130,6 +132,7 @@ public final class PbsFrontendCompiler {
effectiveImportedGlobals,
diagnostics);
flowSemanticsValidator.validate(ast, effectiveSupplementalTopDecls, diagnostics);
lifecycleSemanticsValidator.validate(ast, effectiveSupplementalTopDecls, diagnostics);
if (diagnostics.errorCount() > semanticsErrorBaseline) {
return IRBackendFile.empty(fileId);
}

View File

@ -17,13 +17,15 @@ public final class PbsDeclarationSemanticsValidator {
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 String ATTR_INIT_ALLOWED = "InitAllowed";
private static final Set<String> RESERVED_ATTRIBUTES = Set.of(
ATTR_HOST,
ATTR_CAPABILITY,
ATTR_BUILTIN_TYPE,
ATTR_BUILTIN_CONST,
ATTR_INTRINSIC_CALL);
ATTR_INTRINSIC_CALL,
ATTR_INIT_ALLOWED);
private final NameTable nameTable;
private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator();
@ -400,11 +402,14 @@ public final class PbsDeclarationSemanticsValidator {
final PbsAst.FunctionSignature signature,
final DiagnosticSink diagnostics) {
final var hostAttributes = attributesNamed(signature.attributes(), ATTR_HOST);
final var initAllowedAttributes = attributesNamed(signature.attributes(), ATTR_INIT_ALLOWED);
for (final var attribute : signature.attributes()) {
if (!isReservedAttribute(attribute.name())) {
continue;
}
if (ATTR_HOST.equals(attribute.name()) || ATTR_CAPABILITY.equals(attribute.name())) {
if (ATTR_HOST.equals(attribute.name())
|| ATTR_CAPABILITY.equals(attribute.name())
|| ATTR_INIT_ALLOWED.equals(attribute.name())) {
continue;
}
reportInvalidReservedAttributeTarget(
@ -427,7 +432,16 @@ public final class PbsDeclarationSemanticsValidator {
}
}
if (initAllowedAttributes.size() > 1) {
for (int i = 1; i < initAllowedAttributes.size(); i++) {
reportDuplicateReservedAttribute(initAllowedAttributes.get(i), ATTR_INIT_ALLOWED, diagnostics);
}
}
validateHostAttributeShape(hostAttributes.getFirst(), diagnostics);
if (!initAllowedAttributes.isEmpty()) {
validateInitAllowedAttributeShape(initAllowedAttributes.getFirst(), diagnostics);
}
}
private void validateBuiltinTypeAttribute(
@ -558,6 +572,21 @@ public final class PbsDeclarationSemanticsValidator {
}
}
private void validateInitAllowedAttributeShape(
final PbsAst.Attribute attribute,
final DiagnosticSink diagnostics) {
final var args = validateNamedArguments(attribute, Set.of(), Set.of(), diagnostics);
if (args == null) {
return;
}
if (!args.isEmpty()) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
"InitAllowed does not accept arguments",
attribute.span());
}
}
private Map<String, PbsAst.AttributeValue> validateNamedArguments(
final PbsAst.Attribute attribute,
final Set<String> requiredNames,

View File

@ -0,0 +1,274 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.*;
public final class PbsLifecycleSemanticsValidator {
private static final String ATTR_INIT_ALLOWED = "InitAllowed";
public void validate(
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final p.studio.compiler.source.diagnostics.DiagnosticSink diagnostics) {
final var initFunctions = new ArrayList<PbsAst.FunctionDecl>();
for (final var topDecl : ast.topDecls()) {
if (!(topDecl instanceof PbsAst.FunctionDecl functionDecl)) {
continue;
}
if (functionDecl.lifecycleMarker() == PbsAst.LifecycleMarker.NONE) {
continue;
}
validateLifecycleSignature(functionDecl, diagnostics);
if (functionDecl.lifecycleMarker() == PbsAst.LifecycleMarker.INIT) {
initFunctions.add(functionDecl);
}
}
if (initFunctions.size() > 1) {
for (int i = 1; i < initFunctions.size(); i++) {
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_DUPLICATE_FILE_INIT.name(),
"A source file may declare at most one [Init] function",
initFunctions.get(i).span());
}
}
final var hostPolicy = hostPolicy(ast, supplementalTopDecls);
for (final var initFunction : initFunctions) {
validateInitBody(initFunction.body(), hostPolicy, diagnostics);
}
}
private void validateLifecycleSignature(
final PbsAst.FunctionDecl functionDecl,
final p.studio.compiler.source.diagnostics.DiagnosticSink diagnostics) {
final var valid = functionDecl.parameters().isEmpty()
&& functionDecl.returnKind() == PbsAst.ReturnKind.EXPLICIT_UNIT
&& functionDecl.returnType() != null
&& functionDecl.returnType().kind() == PbsAst.TypeRefKind.UNIT
&& functionDecl.resultErrorType() == null;
if (valid) {
return;
}
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_LIFECYCLE_SIGNATURE.name(),
"Lifecycle functions marked with [%s] must have signature 'fn name() -> void'".formatted(
functionDecl.lifecycleMarker() == PbsAst.LifecycleMarker.INIT ? "Init" : "Frame"),
functionDecl.span());
}
private HostPolicy hostPolicy(
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls) {
final var methodsByOwner = new HashMap<String, Set<String>>();
final var initAllowedByOwner = new HashMap<String, Set<String>>();
collectHostPolicy(ast.topDecls(), methodsByOwner, initAllowedByOwner);
collectHostPolicy(supplementalTopDecls, methodsByOwner, initAllowedByOwner);
return new HostPolicy(methodsByOwner, initAllowedByOwner);
}
private void collectHostPolicy(
final Iterable<PbsAst.TopDecl> topDecls,
final Map<String, Set<String>> methodsByOwner,
final Map<String, Set<String>> initAllowedByOwner) {
for (final var topDecl : topDecls) {
if (!(topDecl instanceof PbsAst.HostDecl hostDecl)) {
continue;
}
for (final var signature : hostDecl.signatures()) {
methodsByOwner.computeIfAbsent(hostDecl.name(), ignored -> new HashSet<>()).add(signature.name());
if (hasInitAllowed(signature.attributes())) {
initAllowedByOwner.computeIfAbsent(hostDecl.name(), ignored -> new HashSet<>()).add(signature.name());
}
}
}
}
private boolean hasInitAllowed(final ReadOnlyList<PbsAst.Attribute> attributes) {
for (final var attribute : attributes) {
if (ATTR_INIT_ALLOWED.equals(attribute.name())) {
return true;
}
}
return false;
}
private void validateInitBody(
final PbsAst.Block block,
final HostPolicy hostPolicy,
final p.studio.compiler.source.diagnostics.DiagnosticSink diagnostics) {
if (block == null) {
return;
}
for (final var statement : block.statements()) {
validateStatement(statement, hostPolicy, diagnostics);
}
validateExpression(block.tailExpression(), hostPolicy, diagnostics);
}
private void validateStatement(
final PbsAst.Statement statement,
final HostPolicy hostPolicy,
final p.studio.compiler.source.diagnostics.DiagnosticSink diagnostics) {
if (statement instanceof PbsAst.LetStatement letStatement) {
validateExpression(letStatement.initializer(), hostPolicy, diagnostics);
return;
}
if (statement instanceof PbsAst.AssignStatement assignStatement) {
validateExpression(assignStatement.value(), hostPolicy, diagnostics);
return;
}
if (statement instanceof PbsAst.ReturnStatement returnStatement) {
validateExpression(returnStatement.value(), hostPolicy, diagnostics);
return;
}
if (statement instanceof PbsAst.IfStatement ifStatement) {
validateExpression(ifStatement.condition(), hostPolicy, diagnostics);
validateInitBody(ifStatement.thenBlock(), hostPolicy, diagnostics);
validateStatement(ifStatement.elseIf(), hostPolicy, diagnostics);
validateInitBody(ifStatement.elseBlock(), hostPolicy, diagnostics);
return;
}
if (statement instanceof PbsAst.ForStatement forStatement) {
validateExpression(forStatement.fromExpression(), hostPolicy, diagnostics);
validateExpression(forStatement.untilExpression(), hostPolicy, diagnostics);
validateExpression(forStatement.stepExpression(), hostPolicy, diagnostics);
validateInitBody(forStatement.body(), hostPolicy, diagnostics);
return;
}
if (statement instanceof PbsAst.WhileStatement whileStatement) {
validateExpression(whileStatement.condition(), hostPolicy, diagnostics);
validateInitBody(whileStatement.body(), hostPolicy, diagnostics);
return;
}
if (statement instanceof PbsAst.ExpressionStatement expressionStatement) {
validateExpression(expressionStatement.expression(), hostPolicy, diagnostics);
}
}
private void validateExpression(
final PbsAst.Expression expression,
final HostPolicy hostPolicy,
final p.studio.compiler.source.diagnostics.DiagnosticSink diagnostics) {
if (expression == null) {
return;
}
if (expression instanceof PbsAst.CallExpr callExpr) {
validateHostCall(callExpr, hostPolicy, diagnostics);
validateExpression(callExpr.callee(), hostPolicy, diagnostics);
for (final var argument : callExpr.arguments()) {
validateExpression(argument, hostPolicy, diagnostics);
}
return;
}
if (expression instanceof PbsAst.ApplyExpr applyExpr) {
validateExpression(applyExpr.callee(), hostPolicy, diagnostics);
validateExpression(applyExpr.argument(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.MemberExpr memberExpr) {
validateExpression(memberExpr.receiver(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.UnaryExpr unaryExpr) {
validateExpression(unaryExpr.expression(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.BinaryExpr binaryExpr) {
validateExpression(binaryExpr.left(), hostPolicy, diagnostics);
validateExpression(binaryExpr.right(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.GroupExpr groupExpr) {
validateExpression(groupExpr.expression(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.NewExpr newExpr) {
for (final var argument : newExpr.arguments()) {
validateExpression(argument, hostPolicy, diagnostics);
}
return;
}
if (expression instanceof PbsAst.IfExpr ifExpr) {
validateExpression(ifExpr.condition(), hostPolicy, diagnostics);
validateInitBody(ifExpr.thenBlock(), hostPolicy, diagnostics);
validateExpression(ifExpr.elseExpression(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.SwitchExpr switchExpr) {
validateExpression(switchExpr.selector(), hostPolicy, diagnostics);
for (final var arm : switchExpr.arms()) {
validateInitBody(arm.block(), hostPolicy, diagnostics);
}
return;
}
if (expression instanceof PbsAst.HandleExpr handleExpr) {
validateExpression(handleExpr.value(), hostPolicy, diagnostics);
for (final var arm : handleExpr.arms()) {
validateInitBody(arm.block(), hostPolicy, diagnostics);
}
return;
}
if (expression instanceof PbsAst.ElseExpr elseExpr) {
validateExpression(elseExpr.optionalExpression(), hostPolicy, diagnostics);
validateExpression(elseExpr.fallbackExpression(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.PropagateExpr propagateExpr) {
validateExpression(propagateExpr.expression(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.BindExpr bindExpr) {
validateExpression(bindExpr.contextExpression(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.AsExpr asExpr) {
validateExpression(asExpr.expression(), hostPolicy, diagnostics);
return;
}
if (expression instanceof PbsAst.BlockExpr blockExpr) {
validateInitBody(blockExpr.block(), hostPolicy, diagnostics);
}
}
private void validateHostCall(
final PbsAst.CallExpr callExpr,
final HostPolicy hostPolicy,
final p.studio.compiler.source.diagnostics.DiagnosticSink diagnostics) {
if (!(callExpr.callee() instanceof PbsAst.MemberExpr memberExpr)) {
return;
}
if (!(memberExpr.receiver() instanceof PbsAst.IdentifierExpr receiverIdentifier)) {
return;
}
if (!hostPolicy.isHostMethod(receiverIdentifier.name(), memberExpr.memberName())) {
return;
}
if (hostPolicy.isInitAllowed(receiverIdentifier.name(), memberExpr.memberName())) {
return;
}
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_INIT_HOST_CALL_NOT_ALLOWED.name(),
"Host call '%s.%s' is not allowed during [Init] without InitAllowed".formatted(
receiverIdentifier.name(),
memberExpr.memberName()),
callExpr.span());
}
private record HostPolicy(
Map<String, Set<String>> methodsByOwner,
Map<String, Set<String>> initAllowedByOwner) {
boolean isHostMethod(final String owner, final String method) {
return methodsByOwner.getOrDefault(owner, Set.of()).contains(method);
}
boolean isInitAllowed(final String owner, final String method) {
return initAllowedByOwner.getOrDefault(owner, Set.of()).contains(method);
}
}
}

View File

@ -15,6 +15,11 @@ public enum PbsSemanticsErrors {
E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE,
E_SEM_DUPLICATE_RESERVED_ATTRIBUTE,
E_SEM_MALFORMED_RESERVED_ATTRIBUTE,
E_SEM_INVALID_LIFECYCLE_SIGNATURE,
E_SEM_DUPLICATE_FILE_INIT,
E_SEM_DUPLICATE_PROJECT_FRAME,
E_SEM_MISSING_PROJECT_FRAME,
E_SEM_INIT_HOST_CALL_NOT_ALLOWED,
E_SEM_MISSING_CONST_TYPE_ANNOTATION,
E_SEM_MISSING_CONST_INITIALIZER,
E_SEM_MISSING_GLOBAL_TYPE_ANNOTATION,

View File

@ -21,6 +21,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
private final PbsFrontendCompiler frontendCompiler;
private final PbsModuleAssemblyService moduleAssemblyService;
private final PbsImportedSemanticContextService importedSemanticContextService;
private final PbsProjectLifecycleSemanticsValidator projectLifecycleSemanticsValidator = new PbsProjectLifecycleSemanticsValidator();
public PBSFrontendPhaseService() {
this(new ResourceStdlibEnvironmentResolver(), new InterfaceModuleLoader());
@ -51,6 +52,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
final var assembly = moduleAssemblyService.assemble(ctx, nameTable, diagnostics, issues);
final var parsedSourceFiles = assembly.parsedSourceFiles();
final var importedSemanticContexts = importedSemanticContextService.build(parsedSourceFiles, assembly.moduleTable());
projectLifecycleSemanticsValidator.validate(parsedSourceFiles.asList(), diagnostics);
final var failedModuleIds = assembly.mutableFailedModuleIds();
final var moduleDependencyGraph = assembly.moduleDependencyGraph();
final var canonicalModulePool = assembly.canonicalModulePool();

View File

@ -0,0 +1,58 @@
package p.studio.compiler.services;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import java.util.ArrayList;
final class PbsProjectLifecycleSemanticsValidator {
void validate(
final java.util.List<PbsParsedSourceFile> parsedSourceFiles,
final DiagnosticSink diagnostics) {
final var frames = new ArrayList<PbsAst.FunctionDecl>();
var sawLifecycleMarker = false;
for (final var parsedSource : parsedSourceFiles) {
if (parsedSource.sourceKind() == SourceKind.SDK_INTERFACE) {
continue;
}
for (final var topDecl : parsedSource.ast().topDecls()) {
if (!(topDecl instanceof PbsAst.FunctionDecl functionDecl)) {
continue;
}
if (functionDecl.lifecycleMarker() == PbsAst.LifecycleMarker.NONE) {
continue;
}
sawLifecycleMarker = true;
if (functionDecl.lifecycleMarker() == PbsAst.LifecycleMarker.FRAME) {
frames.add(functionDecl);
}
}
}
if (!sawLifecycleMarker) {
return;
}
if (frames.isEmpty()) {
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_MISSING_PROJECT_FRAME.name(),
"Lifecycle-marked executable sources must declare exactly one [Frame] function",
parsedSourceFiles.getFirst().ast().span());
return;
}
if (frames.size() <= 1) {
return;
}
for (int i = 1; i < frames.size(); i++) {
p.studio.compiler.source.diagnostics.Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_DUPLICATE_PROJECT_FRAME.name(),
"Executable projects may declare only one [Frame] function",
frames.get(i).span());
}
}
}

View File

@ -203,6 +203,47 @@ class PbsSemanticsDeclarationsTest {
assertEquals(4, invalidCount);
}
@Test
void shouldRejectInvalidLifecycleSignaturesAndMultipleFileInitMarkers() {
final var source = """
[Init]
fn boot(v: int) -> void { return; }
[Init]
fn warmup() -> void { return; }
[Frame]
fn frame() { return; }
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
final var lifecycleSignatureCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_LIFECYCLE_SIGNATURE.name()))
.count();
final var duplicateInitCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_FILE_INIT.name()))
.count();
assertEquals(2, lifecycleSignatureCount);
assertEquals(1, duplicateInitCount);
}
@Test
void shouldRejectInitAllowedOutsideHostSignatures() {
final var source = """
[InitAllowed]
declare const LIMIT: int = 1;
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name())));
}
@Test
void shouldAllowSelfInStructServiceMethodsAndCtors() {
final var source = """

View File

@ -1227,6 +1227,199 @@ class PBSFrontendPhaseServiceTest {
assertEquals(2, cycleCount, diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
}
@Test
void shouldRejectDuplicateFrameMarkersAcrossExecutableProject() throws IOException {
final var projectRoot = tempDir.resolve("project-duplicate-frame-markers");
final var sourceRoot = projectRoot.resolve("src");
final var moduleAPath = sourceRoot.resolve("a");
final var moduleBPath = sourceRoot.resolve("b");
Files.createDirectories(moduleAPath);
Files.createDirectories(moduleBPath);
final var sourceA = moduleAPath.resolve("source.pbs");
final var barrelA = moduleAPath.resolve("mod.barrel");
Files.writeString(sourceA, """
[Frame]
fn frame_a() -> void { return; }
""");
Files.writeString(barrelA, "pub fn frame_a() -> void;");
final var sourceB = moduleBPath.resolve("source.pbs");
final var barrelB = moduleBPath.resolve("mod.barrel");
Files.writeString(sourceB, """
[Frame]
fn frame_b() -> void { return; }
""");
Files.writeString(barrelB, "pub fn frame_b() -> void;");
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, sourceA, fileTable);
registerFile(projectId, projectRoot, barrelA, fileTable);
registerFile(projectId, projectRoot, sourceB, fileTable);
registerFile(projectId, projectRoot, barrelB, fileTable);
final var ctx = new FrontendPhaseContext(
projectTable,
fileTable,
new BuildStack(ReadOnlyList.wrap(List.of(projectId))));
final var diagnostics = DiagnosticSink.empty();
new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
final var duplicateFrameCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_PROJECT_FRAME.name()))
.count();
assertEquals(1, duplicateFrameCount, diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
}
@Test
void shouldRejectHostCallsInInitWithoutInitAllowedAndAcceptWhenPresent() throws IOException {
final var projectRoot = tempDir.resolve("project-init-host-calls");
final var sourceRoot = projectRoot.resolve("src");
Files.createDirectories(sourceRoot);
final var deniedSource = sourceRoot.resolve("denied.pbs");
final var deniedBarrel = sourceRoot.resolve("mod.barrel");
Files.writeString(deniedSource, """
import { Gfx } from @sdk:gfx;
[Init]
fn boot() -> void {
Gfx.clear();
}
[Frame]
fn frame() -> void { return; }
""");
Files.writeString(deniedBarrel, """
pub fn boot() -> void;
pub fn frame() -> void;
""");
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, deniedSource, fileTable);
registerFile(projectId, projectRoot, deniedBarrel, fileTable);
final var sdkModuleDenied = new StdlibModuleSource(
"sdk",
ReadOnlyList.wrap(List.of("gfx")),
ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile(
"gfx.pbs",
"""
declare host Gfx {
[Host(module = "gfx", name = "clear", version = 1)]
fn clear() -> void;
}
"""))),
"pub host Gfx;");
final var deniedFrontendService = new PBSFrontendPhaseService(
resolverForMajor(11, sdkModuleDenied),
new InterfaceModuleLoader(3_200_000));
final var deniedCtx = new FrontendPhaseContext(
projectTable,
fileTable,
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
11);
final var deniedDiagnostics = DiagnosticSink.empty();
deniedFrontendService.compile(
deniedCtx,
deniedDiagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(deniedDiagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INIT_HOST_CALL_NOT_ALLOWED.name())),
deniedDiagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
final var allowedProjectRoot = tempDir.resolve("project-init-host-calls-allowed");
final var allowedSourceRoot = allowedProjectRoot.resolve("src");
Files.createDirectories(allowedSourceRoot);
final var allowedSource = allowedSourceRoot.resolve("main.pbs");
final var allowedBarrel = allowedSourceRoot.resolve("mod.barrel");
Files.writeString(allowedSource, """
import { Gfx } from @sdk:gfx;
[Init]
fn boot() -> void {
Gfx.clear();
}
[Frame]
fn frame() -> void { return; }
""");
Files.writeString(allowedBarrel, """
pub fn boot() -> void;
pub fn frame() -> void;
""");
final var allowedProjectTable = new ProjectTable();
final var allowedFileTable = new FileTable(1);
final var allowedProjectId = allowedProjectTable.register(ProjectDescriptor.builder()
.rootPath(allowedProjectRoot)
.name("app")
.version("1.0.0")
.sourceRoots(ReadOnlyList.wrap(List.of(allowedSourceRoot)))
.build());
registerFile(allowedProjectId, allowedProjectRoot, allowedSource, allowedFileTable);
registerFile(allowedProjectId, allowedProjectRoot, allowedBarrel, allowedFileTable);
final var sdkModuleAllowed = new StdlibModuleSource(
"sdk",
ReadOnlyList.wrap(List.of("gfx")),
ReadOnlyList.wrap(List.of(new StdlibModuleSource.SourceFile(
"gfx.pbs",
"""
declare host Gfx {
[Host(module = "gfx", name = "clear", version = 1)]
[InitAllowed]
fn clear() -> void;
}
"""))),
"pub host Gfx;");
final var allowedFrontendService = new PBSFrontendPhaseService(
resolverForMajor(12, sdkModuleAllowed),
new InterfaceModuleLoader(3_300_000));
final var allowedCtx = new FrontendPhaseContext(
allowedProjectTable,
allowedFileTable,
new BuildStack(ReadOnlyList.wrap(List.of(allowedProjectId))),
12);
final var allowedDiagnostics = DiagnosticSink.empty();
allowedFrontendService.compile(
allowedCtx,
allowedDiagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(allowedDiagnostics.stream().noneMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INIT_HOST_CALL_NOT_ALLOWED.name())),
allowedDiagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
}
private void registerFile(
final ProjectId projectId,
final Path projectRoot,