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 4a8a29b3..967e80d3 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 @@ -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); } 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 e18997e3..d648e779 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 @@ -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 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 validateNamedArguments( final PbsAst.Attribute attribute, final Set requiredNames, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsLifecycleSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsLifecycleSemanticsValidator.java new file mode 100644 index 00000000..c53e5f08 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsLifecycleSemanticsValidator.java @@ -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 supplementalTopDecls, + final p.studio.compiler.source.diagnostics.DiagnosticSink diagnostics) { + final var initFunctions = new ArrayList(); + 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 supplementalTopDecls) { + final var methodsByOwner = new HashMap>(); + final var initAllowedByOwner = new HashMap>(); + collectHostPolicy(ast.topDecls(), methodsByOwner, initAllowedByOwner); + collectHostPolicy(supplementalTopDecls, methodsByOwner, initAllowedByOwner); + return new HostPolicy(methodsByOwner, initAllowedByOwner); + } + + private void collectHostPolicy( + final Iterable topDecls, + final Map> methodsByOwner, + final Map> 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 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> methodsByOwner, + Map> 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); + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java index d52a78ac..8393c07c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java @@ -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, 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 c402b51f..4713ba05 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 @@ -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(); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsProjectLifecycleSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsProjectLifecycleSemanticsValidator.java new file mode 100644 index 00000000..60a388a1 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsProjectLifecycleSemanticsValidator.java @@ -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 parsedSourceFiles, + final DiagnosticSink diagnostics) { + final var frames = new ArrayList(); + 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()); + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsDeclarationsTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsDeclarationsTest.java index 4973e389..36745223 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsDeclarationsTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsDeclarationsTest.java @@ -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 = """ 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 e4a0e82c..49804c0b 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 @@ -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,