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 fcabe4e8..c70c6364 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 @@ -1,6 +1,7 @@ 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.pbs.ast.PbsAst; import p.studio.compiler.pbs.lexer.PbsLexer; @@ -21,10 +22,21 @@ public final class PbsFrontendCompiler { final FileId fileId, final String source, final DiagnosticSink diagnostics) { + return compileFile(fileId, source, diagnostics, SourceKind.PROJECT); + } + + public IRBackendFile compileFile( + final FileId fileId, + final String source, + final DiagnosticSink diagnostics, + final SourceKind sourceKind) { final var admissionBaseline = diagnostics.errorCount(); final var tokens = PbsLexer.lex(source, fileId, diagnostics); - final var ast = PbsParser.parse(tokens, fileId, diagnostics); - final var irBackendFile = compileParsedFile(fileId, ast, 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); if (diagnostics.errorCount() > admissionBaseline) { return IRBackendFile.empty(fileId); } @@ -35,8 +47,16 @@ public final class PbsFrontendCompiler { final FileId fileId, final PbsAst.File ast, final DiagnosticSink diagnostics) { + return compileParsedFile(fileId, ast, diagnostics, SourceKind.PROJECT); + } + + public IRBackendFile compileParsedFile( + final FileId fileId, + final PbsAst.File ast, + final DiagnosticSink diagnostics, + final SourceKind sourceKind) { final var semanticsErrorBaseline = diagnostics.errorCount(); - declarationSemanticsValidator.validate(ast, diagnostics); + declarationSemanticsValidator.validate(ast, sourceKind, diagnostics); flowSemanticsValidator.validate(ast, 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/linking/PbsModuleVisibilityValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java index 05ec71aa..98751e53 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java @@ -245,11 +245,12 @@ public final class PbsModuleVisibilityValidator { continue; } - // TODO(pbs-interface-modules): include top-level host declarations when interface-module mode is added. if (topDecl instanceof PbsAst.StructDecl structDecl) { registerNonFunctionDeclaration(declarations, NonFunctionKind.STRUCT, structDecl.name(), structDecl.span(), nameTable); } else if (topDecl instanceof PbsAst.ContractDecl contractDecl) { registerNonFunctionDeclaration(declarations, NonFunctionKind.CONTRACT, contractDecl.name(), contractDecl.span(), nameTable); + } else if (topDecl instanceof PbsAst.HostDecl hostDecl) { + registerNonFunctionDeclaration(declarations, NonFunctionKind.HOST, hostDecl.name(), hostDecl.span(), nameTable); } else if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { registerNonFunctionDeclaration(declarations, NonFunctionKind.ERROR, errorDecl.name(), errorDecl.span(), nameTable); } else if (topDecl instanceof PbsAst.EnumDecl enumDecl) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCallableScope.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCallableScope.java index 5ca52dbc..3c6372e6 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCallableScope.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCallableScope.java @@ -27,6 +27,14 @@ record PbsCallableScope( return new PbsCallableScope("contract signature scope", "contract", ownerNameId); } + static PbsCallableScope hostMethods(final NameId ownerNameId) { + return new PbsCallableScope("host method scope", "host", ownerNameId); + } + + static PbsCallableScope builtinMethods(final NameId ownerNameId) { + return new PbsCallableScope("builtin method scope", "builtin", ownerNameId); + } + static PbsCallableScope implementsMethods(final NameId ownerNameId) { return new PbsCallableScope("implements method scope", "implements", ownerNameId); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationRuleValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationRuleValidator.java index bc55a568..15d1f727 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationRuleValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationRuleValidator.java @@ -133,6 +133,12 @@ final class PbsDeclarationRuleValidator { } void validateConstDeclaration(final PbsAst.ConstDecl constDecl) { + validateConstDeclaration(constDecl, true); + } + + void validateConstDeclaration( + final PbsAst.ConstDecl constDecl, + final boolean requireInitializer) { if (constDecl.explicitType() == null) { p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, PbsSemanticsErrors.E_SEM_MISSING_CONST_TYPE_ANNOTATION.name(), @@ -146,7 +152,7 @@ final class PbsDeclarationRuleValidator { diagnostics); } - if (constDecl.initializer() == null) { + if (requireInitializer && constDecl.initializer() == null) { p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, PbsSemanticsErrors.E_SEM_MISSING_CONST_INITIALIZER.name(), "Non-builtin const declaration '%s' must include an initializer".formatted(constDecl.name()), 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 b5fcea7c..aa76029e 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 @@ -1,21 +1,56 @@ package p.studio.compiler.pbs.semantics; +import p.studio.compiler.models.SourceKind; import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.source.Span; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.tables.NameTable; import p.studio.utilities.structures.ReadOnlyList; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + public final class PbsDeclarationSemanticsValidator { + private static final String ATTR_HOST = "Host"; + 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_SLOT = "Slot"; + + private static final Set RESERVED_ATTRIBUTES = Set.of( + ATTR_HOST, + ATTR_BUILTIN_TYPE, + ATTR_BUILTIN_CONST, + ATTR_INTRINSIC_CALL, + ATTR_SLOT); + private final NameTable nameTable = new NameTable(); private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator(); public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { + validate(ast, SourceKind.PROJECT, diagnostics); + } + + public void validate( + final PbsAst.File ast, + final SourceKind sourceKind, + final DiagnosticSink diagnostics) { final var binder = new PbsNamespaceBinder(nameTable, diagnostics); final var rules = new PbsDeclarationRuleValidator(nameTable, diagnostics); + final var interfaceModule = sourceKind == SourceKind.SDK_INTERFACE; for (final var topDecl : ast.topDecls()) { if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + if (interfaceModule) { + reportInterfaceNonDeclarativeDecl( + functionDecl.span(), + "Top-level functions are not allowed in interface modules", + diagnostics); + } validateCallable( PbsCallableScope.topLevelFunctions(), functionDecl.name(), @@ -32,6 +67,12 @@ public final class PbsDeclarationSemanticsValidator { if (topDecl instanceof PbsAst.StructDecl structDecl) { binder.registerType(structDecl.name(), structDecl.span(), "struct"); + if (interfaceModule && (structDecl.hasBody() || !structDecl.methods().isEmpty() || !structDecl.ctors().isEmpty())) { + reportInterfaceNonDeclarativeDecl( + structDecl.span(), + "Interface modules must not declare executable struct bodies", + diagnostics); + } validateStructDeclaration(structDecl, binder, rules, diagnostics); continue; } @@ -39,16 +80,39 @@ public final class PbsDeclarationSemanticsValidator { if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { binder.registerType(serviceDecl.name(), serviceDecl.span(), "service"); binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton"); + if (interfaceModule) { + reportInterfaceNonDeclarativeDecl( + serviceDecl.span(), + "Interface modules must not declare executable service bodies", + diagnostics); + } validateServiceDeclaration(serviceDecl, binder, rules); continue; } if (topDecl instanceof PbsAst.ContractDecl contractDecl) { binder.registerType(contractDecl.name(), contractDecl.span(), "contract"); + if (interfaceModule) { + for (final var signature : contractDecl.signatures()) { + validateReservedAttributeTarget(signature.attributes(), "contract signature", diagnostics); + } + } validateContractDeclaration(contractDecl, binder, rules); continue; } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + binder.registerHostOwner(hostDecl.name(), hostDecl.span(), "host owner"); + validateHostDeclaration(hostDecl, binder, rules, interfaceModule, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + binder.registerType(builtinTypeDecl.name(), builtinTypeDecl.span(), "builtin type"); + validateBuiltinTypeDeclaration(builtinTypeDecl, binder, rules, interfaceModule, diagnostics); + continue; + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { binder.registerType(errorDecl.name(), errorDecl.span(), "error"); rules.validateErrorDeclaration(errorDecl); @@ -69,11 +133,18 @@ public final class PbsDeclarationSemanticsValidator { if (topDecl instanceof PbsAst.ConstDecl constDecl) { binder.registerValue(constDecl.name(), constDecl.span(), "const"); - rules.validateConstDeclaration(constDecl); + final var allowMissingInitializer = validateConstDeclarationAttributes(constDecl, interfaceModule, diagnostics); + rules.validateConstDeclaration(constDecl, !allowMissingInitializer); continue; } if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) { + if (interfaceModule) { + reportInterfaceNonDeclarativeDecl( + implementsDecl.span(), + "Interface modules must not declare executable 'implements' blocks", + diagnostics); + } validateImplementsDeclaration(implementsDecl, binder, rules); } } @@ -116,7 +187,7 @@ public final class PbsDeclarationSemanticsValidator { binder.registerCtor(ctorScope, ctor.name(), PbsCallableShape.ctorShapeKey(ctor.parameters()), ctor.span()); if (PbsCtorReturnScanner.containsReturnStatement(ctor.body())) { - p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, PbsSemanticsErrors.E_SEM_INVALID_RETURN_INSIDE_CTOR.name(), "Constructors cannot contain 'return' statements", ctor.span()); @@ -164,6 +235,80 @@ public final class PbsDeclarationSemanticsValidator { } } + private void validateHostDeclaration( + final PbsAst.HostDecl hostDecl, + final PbsNamespaceBinder binder, + final PbsDeclarationRuleValidator rules, + final boolean interfaceModule, + final DiagnosticSink diagnostics) { + if (!interfaceModule) { + reportInterfaceNonDeclarativeDecl( + hostDecl.span(), + "'declare host' is valid only in interface-module sources", + diagnostics); + return; + } + + final var signatureScope = PbsCallableScope.hostMethods(nameTable.register(hostDecl.name())); + for (final var signature : hostDecl.signatures()) { + validateCallable( + signatureScope, + signature.name(), + signature.parameters(), + signature.returnKind(), + signature.returnType(), + signature.resultErrorType(), + signature.span(), + false, + binder, + rules); + validateHostSignatureAttributes(signature, diagnostics); + } + } + + private void validateBuiltinTypeDeclaration( + final PbsAst.BuiltinTypeDecl builtinTypeDecl, + final PbsNamespaceBinder binder, + final PbsDeclarationRuleValidator rules, + final boolean interfaceModule, + final DiagnosticSink diagnostics) { + if (!interfaceModule) { + reportInterfaceNonDeclarativeDecl( + builtinTypeDecl.span(), + "'declare builtin type' is valid only in interface-module sources", + diagnostics); + return; + } + + validateBuiltinTypeAttribute(builtinTypeDecl, diagnostics); + + final var fieldTypes = new ArrayList(builtinTypeDecl.fields().size()); + for (final var field : builtinTypeDecl.fields()) { + fieldTypes.add(field.typeRef()); + validateBuiltinFieldAttributes(field, diagnostics); + } + rules.validateTypeSurfaces( + fieldTypes, + "builtin type '%s'".formatted(builtinTypeDecl.name()), + false); + + final var methodScope = PbsCallableScope.builtinMethods(nameTable.register(builtinTypeDecl.name())); + for (final var signature : builtinTypeDecl.signatures()) { + validateCallable( + methodScope, + signature.name(), + signature.parameters(), + signature.returnKind(), + signature.returnType(), + signature.resultErrorType(), + signature.span(), + false, + binder, + rules); + validateIntrinsicCallAttributes(signature, diagnostics); + } + } + private void validateImplementsDeclaration( final PbsAst.ImplementsDecl implementsDecl, final PbsNamespaceBinder binder, @@ -185,6 +330,404 @@ public final class PbsDeclarationSemanticsValidator { } } + private boolean validateConstDeclarationAttributes( + final PbsAst.ConstDecl constDecl, + final boolean interfaceModule, + final DiagnosticSink diagnostics) { + final var builtinConstAttributes = attributesNamed(constDecl.attributes(), ATTR_BUILTIN_CONST); + for (final var attribute : constDecl.attributes()) { + if (!isReservedAttribute(attribute.name())) { + continue; + } + if (ATTR_BUILTIN_CONST.equals(attribute.name())) { + continue; + } + reportInvalidReservedAttributeTarget( + attribute, + "Attribute '%s' is not valid on const declarations".formatted(attribute.name()), + diagnostics); + } + + if (!interfaceModule && !builtinConstAttributes.isEmpty()) { + for (final var attribute : builtinConstAttributes) { + reportInvalidReservedAttributeTarget( + attribute, + "'BuiltinConst' is valid only in interface-module const declarations", + diagnostics); + } + } + + if (builtinConstAttributes.size() > 1) { + for (int i = 1; i < builtinConstAttributes.size(); i++) { + reportDuplicateReservedAttribute(builtinConstAttributes.get(i), ATTR_BUILTIN_CONST, diagnostics); + } + } + + if (builtinConstAttributes.isEmpty()) { + return false; + } + + validateBuiltinConstAttributeShape(builtinConstAttributes.getFirst(), diagnostics); + if (constDecl.initializer() != null) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), + "Const declaration '%s' with BuiltinConst metadata must omit initializer".formatted(constDecl.name()), + constDecl.span()); + } + return interfaceModule; + } + + private void validateHostSignatureAttributes( + final PbsAst.FunctionSignature signature, + final DiagnosticSink diagnostics) { + final var hostAttributes = attributesNamed(signature.attributes(), ATTR_HOST); + for (final var attribute : signature.attributes()) { + if (!isReservedAttribute(attribute.name())) { + continue; + } + if (ATTR_HOST.equals(attribute.name())) { + continue; + } + reportInvalidReservedAttributeTarget( + attribute, + "Attribute '%s' is not valid on host signatures".formatted(attribute.name()), + diagnostics); + } + + if (hostAttributes.isEmpty()) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name(), + "Host signature '%s' must carry exactly one Host attribute".formatted(signature.name()), + signature.span()); + return; + } + + if (hostAttributes.size() > 1) { + for (int i = 1; i < hostAttributes.size(); i++) { + reportDuplicateReservedAttribute(hostAttributes.get(i), ATTR_HOST, diagnostics); + } + } + + validateHostAttributeShape(hostAttributes.getFirst(), diagnostics); + } + + private void validateBuiltinTypeAttribute( + final PbsAst.BuiltinTypeDecl builtinTypeDecl, + final DiagnosticSink diagnostics) { + final var builtinTypeAttributes = attributesNamed(builtinTypeDecl.attributes(), ATTR_BUILTIN_TYPE); + for (final var attribute : builtinTypeDecl.attributes()) { + if (!isReservedAttribute(attribute.name())) { + continue; + } + if (ATTR_BUILTIN_TYPE.equals(attribute.name())) { + continue; + } + reportInvalidReservedAttributeTarget( + attribute, + "Attribute '%s' is not valid on builtin type declarations".formatted(attribute.name()), + diagnostics); + } + + if (builtinTypeAttributes.isEmpty()) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name(), + "Builtin type '%s' must carry exactly one BuiltinType attribute".formatted(builtinTypeDecl.name()), + builtinTypeDecl.span()); + return; + } + + if (builtinTypeAttributes.size() > 1) { + for (int i = 1; i < builtinTypeAttributes.size(); i++) { + reportDuplicateReservedAttribute(builtinTypeAttributes.get(i), ATTR_BUILTIN_TYPE, diagnostics); + } + } + + validateBuiltinTypeAttributeShape(builtinTypeAttributes.getFirst(), diagnostics); + } + + private void validateBuiltinFieldAttributes( + final PbsAst.BuiltinFieldDecl field, + final DiagnosticSink diagnostics) { + final var slotAttributes = attributesNamed(field.attributes(), ATTR_SLOT); + for (final var attribute : field.attributes()) { + if (!isReservedAttribute(attribute.name())) { + continue; + } + if (ATTR_SLOT.equals(attribute.name())) { + continue; + } + reportInvalidReservedAttributeTarget( + attribute, + "Attribute '%s' is not valid on builtin fields".formatted(attribute.name()), + diagnostics); + } + + if (slotAttributes.size() > 1) { + for (int i = 1; i < slotAttributes.size(); i++) { + reportDuplicateReservedAttribute(slotAttributes.get(i), ATTR_SLOT, diagnostics); + } + } + + if (!slotAttributes.isEmpty()) { + validateSlotAttributeShape(slotAttributes.getFirst(), diagnostics); + } + } + + private void validateIntrinsicCallAttributes( + final PbsAst.FunctionSignature signature, + final DiagnosticSink diagnostics) { + final var intrinsicAttributes = attributesNamed(signature.attributes(), ATTR_INTRINSIC_CALL); + for (final var attribute : signature.attributes()) { + if (!isReservedAttribute(attribute.name())) { + continue; + } + if (ATTR_INTRINSIC_CALL.equals(attribute.name())) { + continue; + } + reportInvalidReservedAttributeTarget( + attribute, + "Attribute '%s' is not valid on builtin method signatures".formatted(attribute.name()), + diagnostics); + } + + if (intrinsicAttributes.size() > 1) { + for (int i = 1; i < intrinsicAttributes.size(); i++) { + reportDuplicateReservedAttribute(intrinsicAttributes.get(i), ATTR_INTRINSIC_CALL, diagnostics); + } + } + + if (!intrinsicAttributes.isEmpty()) { + validateIntrinsicCallAttributeShape(intrinsicAttributes.getFirst(), diagnostics); + } + } + + private void validateHostAttributeShape( + final PbsAst.Attribute attribute, + final DiagnosticSink diagnostics) { + final var args = validateNamedArguments(attribute, Set.of("module", "name", "version"), Set.of(), diagnostics); + if (args == null) { + return; + } + + validateRequiredStringArgument(attribute, args, "module", false, diagnostics); + validateRequiredStringArgument(attribute, args, "name", false, diagnostics); + validateRequiredIntArgument(attribute, args, "version", true, diagnostics); + } + + private void validateBuiltinTypeAttributeShape( + final PbsAst.Attribute attribute, + final DiagnosticSink diagnostics) { + final var args = validateNamedArguments(attribute, Set.of("name", "version"), Set.of(), diagnostics); + if (args == null) { + return; + } + + validateRequiredStringArgument(attribute, args, "name", false, diagnostics); + validateRequiredIntArgument(attribute, args, "version", true, diagnostics); + } + + private void validateBuiltinConstAttributeShape( + final PbsAst.Attribute attribute, + final DiagnosticSink diagnostics) { + final var args = validateNamedArguments(attribute, Set.of("target", "name", "version"), Set.of(), diagnostics); + if (args == null) { + return; + } + + validateRequiredStringArgument(attribute, args, "target", false, diagnostics); + validateRequiredStringArgument(attribute, args, "name", false, diagnostics); + validateRequiredIntArgument(attribute, args, "version", true, diagnostics); + } + + private void validateIntrinsicCallAttributeShape( + final PbsAst.Attribute attribute, + final DiagnosticSink diagnostics) { + final var args = validateNamedArguments(attribute, Set.of("name"), Set.of("version"), diagnostics); + if (args == null) { + return; + } + + validateRequiredStringArgument(attribute, args, "name", false, diagnostics); + if (args.containsKey("version")) { + validateRequiredIntArgument(attribute, args, "version", true, diagnostics); + } + } + + private void validateSlotAttributeShape( + final PbsAst.Attribute attribute, + final DiagnosticSink diagnostics) { + final var args = validateNamedArguments(attribute, Set.of("index"), Set.of(), diagnostics); + if (args == null) { + return; + } + + validateRequiredIntArgument(attribute, args, "index", false, diagnostics); + } + + private Map validateNamedArguments( + final PbsAst.Attribute attribute, + final Set requiredNames, + final Set optionalNames, + final DiagnosticSink diagnostics) { + final var allowedNames = new HashSet(); + allowedNames.addAll(requiredNames); + allowedNames.addAll(optionalNames); + + final var arguments = new HashMap(); + var malformed = false; + for (final var argument : attribute.arguments()) { + if (!allowedNames.contains(argument.name())) { + malformed = true; + continue; + } + if (arguments.putIfAbsent(argument.name(), argument.value()) != null) { + malformed = true; + } + } + + for (final var requiredName : requiredNames) { + if (!arguments.containsKey(requiredName)) { + malformed = true; + } + } + + if (malformed) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), + "Malformed %s attribute argument set".formatted(attribute.name()), + attribute.span()); + return null; + } + return arguments; + } + + private void validateRequiredStringArgument( + final PbsAst.Attribute attribute, + final Map arguments, + final String argumentName, + final boolean positiveOnly, + final DiagnosticSink diagnostics) { + final var value = arguments.get(argumentName); + if (!(value instanceof PbsAst.AttributeStringValue stringValue)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), + "Malformed %s.%s attribute argument".formatted(attribute.name(), argumentName), + attribute.span()); + return; + } + + if (isStringLiteralEmpty(stringValue.value())) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), + "Attribute %s.%s must be a non-empty string".formatted(attribute.name(), argumentName), + attribute.span()); + } + + if (positiveOnly && stringValue.value().isBlank()) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), + "Attribute %s.%s must be a positive string literal".formatted(attribute.name(), argumentName), + attribute.span()); + } + } + + private void validateRequiredIntArgument( + final PbsAst.Attribute attribute, + final Map arguments, + final String argumentName, + final boolean positive, + final DiagnosticSink diagnostics) { + final var value = arguments.get(argumentName); + if (!(value instanceof PbsAst.AttributeIntValue intValue)) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), + "Malformed %s.%s attribute argument".formatted(attribute.name(), argumentName), + attribute.span()); + return; + } + + final var numericValue = intValue.value(); + final boolean invalid = positive ? numericValue <= 0 : numericValue < 0; + if (invalid) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(), + "Attribute %s.%s has invalid numeric value".formatted(attribute.name(), argumentName), + attribute.span()); + } + } + + private boolean isStringLiteralEmpty(final String lexeme) { + if (lexeme == null) { + return true; + } + final var trimmed = lexeme.trim(); + if (trimmed.length() >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) { + return trimmed.length() == 2; + } + return trimmed.isEmpty(); + } + + private List attributesNamed( + final ReadOnlyList attributes, + final String name) { + final var matches = new ArrayList(); + for (final var attribute : attributes) { + if (name.equals(attribute.name())) { + matches.add(attribute); + } + } + return matches; + } + + private boolean isReservedAttribute(final String name) { + return RESERVED_ATTRIBUTES.contains(name); + } + + private void validateReservedAttributeTarget( + final ReadOnlyList attributes, + final String targetSurface, + final DiagnosticSink diagnostics) { + for (final var attribute : attributes) { + if (!isReservedAttribute(attribute.name())) { + continue; + } + reportInvalidReservedAttributeTarget( + attribute, + "Attribute '%s' is not valid on %s".formatted(attribute.name(), targetSurface), + diagnostics); + } + } + + private void reportInvalidReservedAttributeTarget( + final PbsAst.Attribute attribute, + final String message, + final DiagnosticSink diagnostics) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name(), + message, + attribute.span()); + } + + private void reportDuplicateReservedAttribute( + final PbsAst.Attribute attribute, + final String attributeName, + final DiagnosticSink diagnostics) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name(), + "Duplicate %s attribute on the same declaration surface".formatted(attributeName), + attribute.span()); + } + + private void reportInterfaceNonDeclarativeDecl( + final Span span, + final String message, + final DiagnosticSink diagnostics) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name(), + message, + span); + } + private void validateCallable( final PbsCallableScope scope, final String callableName, 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 c2446cbd..a0845b11 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 @@ -10,6 +10,11 @@ public enum PbsSemanticsErrors { E_SEM_DUPLICATE_ENUM_CASE_ID, E_SEM_INVALID_MIXED_OPTIONAL_RESULT_RETURN, E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE, + E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION, + E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET, + E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE, + E_SEM_DUPLICATE_RESERVED_ATTRIBUTE, + E_SEM_MALFORMED_RESERVED_ATTRIBUTE, E_SEM_MISSING_CONST_TYPE_ANNOTATION, E_SEM_MISSING_CONST_INITIALIZER, E_SEM_CONST_NON_CONSTANT_INITIALIZER, 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 5baca79c..98f3597f 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 @@ -134,7 +134,11 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { if (failedModuleKeys.contains(parsedSource.moduleKey())) { continue; } - final var irBackendFile = frontendCompiler.compileParsedFile(parsedSource.fileId(), parsedSource.ast(), diagnostics); + final var irBackendFile = frontendCompiler.compileParsedFile( + parsedSource.fileId(), + parsedSource.ast(), + diagnostics, + parsedSource.sourceKind()); if (parsedSource.sourceKind() == SourceKind.SDK_INTERFACE) { continue; } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java index 68b269dc..86f800dd 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityTest.java @@ -215,6 +215,66 @@ class PbsModuleVisibilityTest { d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name()))); } + @Test + void shouldResolveHostBarrelEntryWhenHostDeclarationExistsInInterfaceModule() { + final var diagnostics = DiagnosticSink.empty(); + final var sourceFileId = new FileId(100); + final var barrelFileId = new FileId(101); + final var sourceAst = parseSource( + """ + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int) -> void; + } + """, + sourceFileId, + diagnostics, + PbsParser.ParseMode.INTERFACE_MODULE); + final var barrelAst = parseBarrel( + """ + pub host Gfx; + """, + barrelFileId, + diagnostics); + final var module = new PbsModuleVisibilityValidator.ModuleUnit( + new PbsModuleVisibilityValidator.ModuleCoordinates("sdk", ReadOnlyList.wrap(List.of("gfx"))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceFileId, sourceAst))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(barrelFileId, barrelAst)))); + + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(module)), diagnostics); + + assertTrue(diagnostics.stream().noneMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name()))); + } + + @Test + void shouldRejectHostBarrelEntryWhenHostDeclarationIsMissing() { + final var diagnostics = DiagnosticSink.empty(); + final var sourceFileId = new FileId(110); + final var barrelFileId = new FileId(111); + final var sourceAst = parseSource( + """ + declare enum Color(Red); + """, + sourceFileId, + diagnostics); + final var barrelAst = parseBarrel( + """ + pub host Gfx; + """, + barrelFileId, + diagnostics); + final var module = new PbsModuleVisibilityValidator.ModuleUnit( + new PbsModuleVisibilityValidator.ModuleCoordinates("sdk", ReadOnlyList.wrap(List.of("gfx"))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.SourceFile(sourceFileId, sourceAst))), + ReadOnlyList.wrap(List.of(new PbsModuleVisibilityValidator.BarrelFile(barrelFileId, barrelAst)))); + + new PbsModuleVisibilityValidator().validate(ReadOnlyList.wrap(List.of(module)), diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsLinkErrors.E_LINK_UNRESOLVED_BARREL_ENTRY.name()))); + } + private PbsModuleVisibilityValidator.ModuleUnit module( final String project, final String modulePath, @@ -257,7 +317,15 @@ class PbsModuleVisibilityTest { final String source, final FileId fileId, final DiagnosticSink diagnostics) { - return PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + return parseSource(source, fileId, diagnostics, PbsParser.ParseMode.ORDINARY); + } + + private PbsAst.File parseSource( + final String source, + final FileId fileId, + final DiagnosticSink diagnostics, + final PbsParser.ParseMode parseMode) { + return PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics, parseMode); } private PbsAst.BarrelFile parseBarrel( diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java new file mode 100644 index 00000000..034f9b05 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java @@ -0,0 +1,101 @@ +package p.studio.compiler.pbs.semantics; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.models.SourceKind; +import p.studio.compiler.pbs.PbsFrontendCompiler; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PbsInterfaceModuleSemanticsTest { + + @Test + void shouldAcceptValidInterfaceModuleReservedMetadataShapes() { + final var source = """ + [BuiltinType(name = "color", version = 1)] + declare builtin type Color( + [Slot(index = 0)] pub raw: int + ) { + [IntrinsicCall(name = "pack", version = 1)] + fn pack() -> int; + } + + declare host Gfx { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn draw_pixel(x: int, y: int, c: Color) -> void; + } + + [BuiltinConst(target = "color", name = "white", version = 1)] + declare const WHITE: Color; + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics, SourceKind.SDK_INTERFACE); + + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name()))); + } + + @Test + void shouldRejectInvalidReservedAttributeTargetsAndShapesInInterfaceModule() { + final var source = """ + declare host Gfx { + fn draw_pixel(x: int, y: int) -> void; + } + + declare contract Api { + [Host(module = "gfx", name = "draw_pixel", version = 1)] + fn run() -> void; + } + + declare builtin type Color(pub raw: int) { + [IntrinsicCall(version = 1)] + fn pack() -> int; + } + + [BuiltinConst(target = "", name = "white", version = 0)] + declare const WHITE: int = 1; + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics, SourceKind.SDK_INTERFACE); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_MISSING_REQUIRED_RESERVED_ATTRIBUTE.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name()))); + } + + @Test + void shouldRejectExecutableDeclarationsInInterfaceModule() { + final var source = """ + fn run() -> int { return 1; } + + declare service Game { + fn tick() -> int { return 1; } + } + + declare contract C { fn run() -> int; } + declare struct S(v: int); + implements C for S using s { + fn run() -> int { return 1; } + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics, SourceKind.SDK_INTERFACE); + + final var nonDeclarativeCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name())) + .count(); + assertTrue(nonDeclarativeCount >= 3); + } +} 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 f028f6c8..9939d0d2 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 @@ -13,6 +13,7 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironment; import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver; import p.studio.compiler.pbs.stdlib.StdlibModuleSource; import p.studio.compiler.pbs.linking.PbsLinkErrors; +import p.studio.compiler.pbs.semantics.PbsSemanticsErrors; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.ProjectId; import p.studio.compiler.source.tables.FileTable; @@ -219,7 +220,8 @@ class PBSFrontendPhaseServiceTest { LogAggregator.empty(), BuildingIssueSink.empty()); - assertTrue(diagnostics.isEmpty()); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name()))); assertEquals(0, irBackend.getFunctions().size()); }