implements PR025

This commit is contained in:
bQUARKz 2026-03-06 13:14:58 +00:00
parent 0770bb869a
commit 52e8746bcc
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 768 additions and 10 deletions

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}

View File

@ -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()),

View File

@ -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<String> 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<PbsAst.TypeRef>(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<String, PbsAst.AttributeValue> validateNamedArguments(
final PbsAst.Attribute attribute,
final Set<String> requiredNames,
final Set<String> optionalNames,
final DiagnosticSink diagnostics) {
final var allowedNames = new HashSet<String>();
allowedNames.addAll(requiredNames);
allowedNames.addAll(optionalNames);
final var arguments = new HashMap<String, PbsAst.AttributeValue>();
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<String, PbsAst.AttributeValue> 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<String, PbsAst.AttributeValue> 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<PbsAst.Attribute> attributesNamed(
final ReadOnlyList<PbsAst.Attribute> attributes,
final String name) {
final var matches = new ArrayList<PbsAst.Attribute>();
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<PbsAst.Attribute> 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,

View File

@ -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,

View File

@ -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;
}

View File

@ -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(

View File

@ -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);
}
}

View File

@ -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());
}