From 62a04053539403b3193266552c46f789802df4a3 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 5 Mar 2026 12:55:00 +0000 Subject: [PATCH] implements PR007 --- .../compiler/pbs/PbsFrontendCompiler.java | 25 +- .../PbsDeclarationSemanticsValidator.java | 669 ++++++++++++++++++ .../pbs/semantics/PbsSemanticsErrors.java | 16 + .../compiler/pbs/PbsFrontendCompilerTest.java | 19 +- .../PbsSemanticsDeclarationsTest.java | 147 ++++ 5 files changed, 854 insertions(+), 22 deletions(-) create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java create mode 100644 prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsDeclarationsTest.java 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 0dc6a214..453236ee 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 @@ -5,17 +5,16 @@ import p.studio.compiler.models.IRBackendFile; import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.lexer.PbsLexer; import p.studio.compiler.pbs.parser.PbsParser; +import p.studio.compiler.pbs.semantics.PbsDeclarationSemanticsValidator; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; -import p.studio.compiler.source.identifiers.NameId; -import p.studio.compiler.source.tables.NameTable; import p.studio.utilities.structures.ReadOnlyList; import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; public final class PbsFrontendCompiler { + private final PbsDeclarationSemanticsValidator declarationSemanticsValidator = new PbsDeclarationSemanticsValidator(); + public IRBackendFile compileFile( final FileId fileId, final String source, @@ -29,26 +28,10 @@ public final class PbsFrontendCompiler { final FileId fileId, final PbsAst.File ast, final DiagnosticSink diagnostics) { - validateFunctionNames(ast, diagnostics); + declarationSemanticsValidator.validate(ast, diagnostics); return new IRBackendFile(fileId, lowerFunctions(fileId, ast)); } - private void validateFunctionNames( - final PbsAst.File ast, - final DiagnosticSink diagnostics) { - final var nameTable = new NameTable(); - final Set nameIds = new HashSet<>(); - for (final var fn : ast.functions()) { - final var nameId = nameTable.register(fn.name()); - if (nameIds.add(nameId)) { - continue; - } - diagnostics.error("E_RESOLVE_DUPLICATE_SYMBOL", - "Duplicate function '%s'".formatted(fn.name()), - fn.span()); - } - } - private ReadOnlyList lowerFunctions(final FileId fileId, final PbsAst.File ast) { final var functions = new ArrayList(ast.functions().size()); for (final var fn : ast.functions()) { 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 new file mode 100644 index 00000000..ec49158a --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java @@ -0,0 +1,669 @@ +package p.studio.compiler.pbs.semantics; + +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.diagnostics.RelatedSpan; +import p.studio.compiler.source.diagnostics.Severity; +import p.studio.compiler.source.identifiers.NameId; +import p.studio.compiler.source.tables.NameTable; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class PbsDeclarationSemanticsValidator { + private final NameTable nameTable = new NameTable(); + + public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { + final var binder = new NamespaceBinder(nameTable, diagnostics); + + for (final var topDecl : ast.topDecls()) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + validateCallable( + CallableScope.topLevelFunctions(), + functionDecl.name(), + functionDecl.parameters(), + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + functionDecl.span(), + binder, + diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.StructDecl structDecl) { + binder.registerType(structDecl.name(), structDecl.span(), "struct"); + validateStructDeclaration(structDecl, binder, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + binder.registerType(serviceDecl.name(), serviceDecl.span(), "service"); + binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton"); + validateServiceDeclaration(serviceDecl, binder, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + binder.registerType(contractDecl.name(), contractDecl.span(), "contract"); + validateContractDeclaration(contractDecl, binder, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + binder.registerType(errorDecl.name(), errorDecl.span(), "error"); + validateErrorDeclaration(errorDecl, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + binder.registerType(enumDecl.name(), enumDecl.span(), "enum"); + validateEnumDeclaration(enumDecl, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + binder.registerType(callbackDecl.name(), callbackDecl.span(), "callback"); + validateCallbackDeclaration(callbackDecl, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + binder.registerValue(constDecl.name(), constDecl.span(), "const"); + validateConstDeclaration(constDecl, diagnostics); + continue; + } + + if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) { + validateImplementsDeclaration(implementsDecl, binder, diagnostics); + } + } + } + + private void validateStructDeclaration( + final PbsAst.StructDecl structDecl, + final NamespaceBinder binder, + final DiagnosticSink diagnostics) { + final var methodScope = CallableScope.structMethods(nameTable.register(structDecl.name())); + for (final var method : structDecl.methods()) { + validateCallable( + methodScope, + method.name(), + method.parameters(), + method.returnKind(), + method.returnType(), + method.resultErrorType(), + method.span(), + binder, + diagnostics); + } + + final var ctorScope = CallableScope.structCtors(nameTable.register(structDecl.name())); + for (final var ctor : structDecl.ctors()) { + validateParameters(ctor.parameters(), "ctor '%s'".formatted(ctor.name()), diagnostics); + validateTypeSurfaceRecursive( + ctor.parameters().stream().map(PbsAst.Parameter::typeRef).toList(), + "ctor '%s'".formatted(ctor.name()), + diagnostics); + + binder.registerCtor(ctorScope, ctor.name(), ctorShapeKey(ctor.parameters()), ctor.span()); + if (containsReturnStatement(ctor.body())) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_RETURN_INSIDE_CTOR.name(), + "Constructors cannot contain 'return' statements", + ctor.span()); + } + } + } + + private void validateServiceDeclaration( + final PbsAst.ServiceDecl serviceDecl, + final NamespaceBinder binder, + final DiagnosticSink diagnostics) { + final var methodScope = CallableScope.serviceMethods(nameTable.register(serviceDecl.name())); + for (final var method : serviceDecl.methods()) { + validateCallable( + methodScope, + method.name(), + method.parameters(), + method.returnKind(), + method.returnType(), + method.resultErrorType(), + method.span(), + binder, + diagnostics); + } + } + + private void validateContractDeclaration( + final PbsAst.ContractDecl contractDecl, + final NamespaceBinder binder, + final DiagnosticSink diagnostics) { + final var signatureScope = CallableScope.contractSignatures(nameTable.register(contractDecl.name())); + for (final var signature : contractDecl.signatures()) { + validateCallable( + signatureScope, + signature.name(), + signature.parameters(), + signature.returnKind(), + signature.returnType(), + signature.resultErrorType(), + signature.span(), + binder, + diagnostics); + } + } + + private void validateErrorDeclaration( + final PbsAst.ErrorDecl errorDecl, + final DiagnosticSink diagnostics) { + final var seenCases = new HashMap(); + for (final var caseName : errorDecl.cases()) { + final var caseNameId = nameTable.register(caseName); + final var first = seenCases.putIfAbsent(caseNameId, errorDecl.span()); + if (first != null) { + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_ERROR_CASE_LABEL.name(), + "Duplicate error case label '%s' in '%s'".formatted(caseName, errorDecl.name()), + errorDecl.span(), + List.of(new RelatedSpan("First case is here", first))); + } + } + } + + private void validateEnumDeclaration( + final PbsAst.EnumDecl enumDecl, + final DiagnosticSink diagnostics) { + final var seenLabels = new HashMap(); + final var seenIds = new HashMap(); + for (final var enumCase : enumDecl.cases()) { + final var labelId = nameTable.register(enumCase.name()); + final var firstLabel = seenLabels.putIfAbsent(labelId, enumCase.span()); + if (firstLabel != null) { + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_LABEL.name(), + "Duplicate enum case label '%s' in '%s'".formatted(enumCase.name(), enumDecl.name()), + enumCase.span(), + List.of(new RelatedSpan("First case is here", firstLabel))); + } + + if (enumCase.explicitValue() != null) { + final var firstId = seenIds.putIfAbsent(enumCase.explicitValue(), enumCase.span()); + if (firstId != null) { + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_ID.name(), + "Duplicate enum case id '%s' in '%s'".formatted(enumCase.explicitValue(), enumDecl.name()), + enumCase.span(), + List.of(new RelatedSpan("First id is here", firstId))); + } + } + } + } + + private void validateCallbackDeclaration( + final PbsAst.CallbackDecl callbackDecl, + final DiagnosticSink diagnostics) { + validateParameters(callbackDecl.parameters(), "callback '%s'".formatted(callbackDecl.name()), diagnostics); + validateReturnSurface( + callbackDecl.returnKind(), + callbackDecl.returnType(), + callbackDecl.resultErrorType(), + "callback '%s'".formatted(callbackDecl.name()), + callbackDecl.span(), + diagnostics); + validateTypeSurfaceRecursive( + callbackDecl.parameters().stream().map(PbsAst.Parameter::typeRef).toList(), + "callback '%s'".formatted(callbackDecl.name()), + diagnostics); + } + + private void validateConstDeclaration( + final PbsAst.ConstDecl constDecl, + final DiagnosticSink diagnostics) { + if (constDecl.explicitType() == null) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_MISSING_CONST_TYPE_ANNOTATION.name(), + "Const declaration '%s' must include an explicit type annotation".formatted(constDecl.name()), + constDecl.span()); + } else { + validateTypeSurfaceRecursive( + List.of(constDecl.explicitType()), + "const '%s'".formatted(constDecl.name()), + diagnostics); + } + + if (constDecl.initializer() == null) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_MISSING_CONST_INITIALIZER.name(), + "Non-builtin const declaration '%s' must include an initializer".formatted(constDecl.name()), + constDecl.span()); + } + } + + private void validateImplementsDeclaration( + final PbsAst.ImplementsDecl implementsDecl, + final NamespaceBinder binder, + final DiagnosticSink diagnostics) { + final var ownerNameId = nameTable.register(implementsDecl.ownerName() + ":" + implementsDecl.contractName()); + final var scope = CallableScope.implementsMethods(ownerNameId); + for (final var method : implementsDecl.methods()) { + validateCallable( + scope, + method.name(), + method.parameters(), + method.returnKind(), + method.returnType(), + method.resultErrorType(), + method.span(), + binder, + diagnostics); + } + } + + private void validateCallable( + final CallableScope scope, + final String callableName, + final ReadOnlyList parameters, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final Span span, + final NamespaceBinder binder, + final DiagnosticSink diagnostics) { + validateParameters(parameters, "callable '%s'".formatted(callableName), diagnostics); + validateReturnSurface(returnKind, returnType, resultErrorType, "callable '%s'".formatted(callableName), span, diagnostics); + validateTypeSurfaceRecursive(parameters.stream().map(PbsAst.Parameter::typeRef).toList(), "callable '%s'".formatted(callableName), diagnostics); + + binder.registerCallable(scope, callableName, callableShapeKey(parameters, returnKind, returnType, resultErrorType), span); + } + + private void validateParameters( + final ReadOnlyList parameters, + final String ownerDescription, + final DiagnosticSink diagnostics) { + final var seen = new HashMap(); + for (final var parameter : parameters) { + final var parameterNameId = nameTable.register(parameter.name()); + final var first = seen.putIfAbsent(parameterNameId, parameter.span()); + if (first != null) { + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_PARAMETER_NAME.name(), + "Duplicate parameter name '%s' in %s".formatted(parameter.name(), ownerDescription), + parameter.span(), + List.of(new RelatedSpan("First parameter is here", first))); + } + } + } + + private void validateReturnSurface( + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final String ownerDescription, + final Span span, + final DiagnosticSink diagnostics) { + if (returnKind == PbsAst.ReturnKind.RESULT && isOptionalType(returnType)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MIXED_OPTIONAL_RESULT_RETURN.name(), + "Return surface for %s cannot combine 'result' with top-level 'optional' payload".formatted(ownerDescription), + span); + } + + if (returnType != null && returnType.kind() == PbsAst.TypeRefKind.NAMED_TUPLE) { + validateDuplicateOutputLabels(returnType, ownerDescription, diagnostics); + } + + if (returnType != null) { + validateTypeSurfaceRecursive(List.of(returnType), ownerDescription, diagnostics); + } + if (resultErrorType != null) { + validateTypeSurfaceRecursive(List.of(resultErrorType), ownerDescription, diagnostics); + } + } + + private void validateDuplicateOutputLabels( + final PbsAst.TypeRef namedTupleReturnType, + final String ownerDescription, + final DiagnosticSink diagnostics) { + final var seen = new HashMap(); + for (final var field : namedTupleReturnType.fields()) { + final var labelId = nameTable.register(field.label()); + final var first = seen.putIfAbsent(labelId, field.span()); + if (first != null) { + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_RETURN_LABEL.name(), + "Duplicate return label '%s' in %s".formatted(field.label(), ownerDescription), + field.span(), + List.of(new RelatedSpan("First label is here", first))); + } + } + } + + private void validateTypeSurfaceRecursive( + final List roots, + final String ownerDescription, + final DiagnosticSink diagnostics) { + final var pending = new ArrayList<>(roots); + while (!pending.isEmpty()) { + final var typeRef = pending.removeLast(); + if (typeRef == null) { + continue; + } + + switch (typeRef.kind()) { + case OPTIONAL -> { + if (typeRef.inner() == null || typeRef.inner().kind() == PbsAst.TypeRefKind.UNIT) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE.name(), + "Invalid optional type surface in %s: 'optional void' is not allowed".formatted(ownerDescription), + typeRef.span()); + } + pending.add(typeRef.inner()); + } + case GROUP -> pending.add(typeRef.inner()); + case NAMED_TUPLE -> { + for (final var field : typeRef.fields()) { + pending.add(field.typeRef()); + } + } + default -> { + // No recursive children. + } + } + } + } + + private boolean isOptionalType(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return false; + } + if (typeRef.kind() == PbsAst.TypeRefKind.OPTIONAL) { + return true; + } + if (typeRef.kind() == PbsAst.TypeRefKind.GROUP) { + return isOptionalType(typeRef.inner()); + } + return false; + } + + private boolean containsReturnStatement(final PbsAst.Block block) { + if (block == null) { + return false; + } + + for (final var statement : block.statements()) { + if (statement instanceof PbsAst.ReturnStatement) { + return true; + } + if (statement instanceof PbsAst.IfStatement ifStatement) { + if (containsReturnInIfStatement(ifStatement)) { + return true; + } + continue; + } + if (statement instanceof PbsAst.ForStatement forStatement && containsReturnStatement(forStatement.body())) { + return true; + } + if (statement instanceof PbsAst.WhileStatement whileStatement && containsReturnStatement(whileStatement.body())) { + return true; + } + if (statement instanceof PbsAst.ExpressionStatement expressionStatement && containsReturnExpression(expressionStatement.expression())) { + return true; + } + } + + return block.tailExpression() != null && containsReturnExpression(block.tailExpression()); + } + + private boolean containsReturnInIfStatement(final PbsAst.IfStatement ifStatement) { + if (ifStatement == null) { + return false; + } + if (containsReturnStatement(ifStatement.thenBlock()) || containsReturnStatement(ifStatement.elseBlock())) { + return true; + } + return containsReturnInIfStatement(ifStatement.elseIf()); + } + + private boolean containsReturnExpression(final PbsAst.Expression expression) { + if (expression == null) { + return false; + } + if (expression instanceof PbsAst.BlockExpr blockExpr) { + return containsReturnStatement(blockExpr.block()); + } + if (expression instanceof PbsAst.IfExpr ifExpr) { + return containsReturnStatement(ifExpr.thenBlock()) || containsReturnExpression(ifExpr.elseExpression()); + } + if (expression instanceof PbsAst.SwitchExpr switchExpr) { + for (final var arm : switchExpr.arms()) { + if (containsReturnStatement(arm.block())) { + return true; + } + } + } + if (expression instanceof PbsAst.HandleExpr handleExpr) { + for (final var arm : handleExpr.arms()) { + if (containsReturnStatement(arm.block())) { + return true; + } + } + } + return false; + } + + private String callableShapeKey( + final ReadOnlyList parameters, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + final var builder = new StringBuilder(); + builder.append('('); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) { + builder.append(','); + } + builder.append(typeKey(parameters.get(i).typeRef())); + } + builder.append(")->"); + builder.append(outputSurfaceKey(returnKind, returnType, resultErrorType)); + return builder.toString(); + } + + private String ctorShapeKey(final ReadOnlyList parameters) { + final var builder = new StringBuilder(); + builder.append('('); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) { + builder.append(','); + } + builder.append(typeKey(parameters.get(i).typeRef())); + } + builder.append(')'); + return builder.toString(); + } + + private String outputSurfaceKey( + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + return switch (returnKind) { + case INFERRED_UNIT, EXPLICIT_UNIT -> "unit"; + case PLAIN -> carrierTypeKey(returnType); + case RESULT -> "result<" + typeKey(resultErrorType) + ">" + carrierTypeKey(returnType); + }; + } + + private String carrierTypeKey(final PbsAst.TypeRef typeRef) { + final var unwrapped = unwrapGroup(typeRef); + if (unwrapped != null && unwrapped.kind() == PbsAst.TypeRefKind.NAMED_TUPLE && unwrapped.fields().size() == 1) { + return typeKey(unwrapped.fields().getFirst().typeRef()); + } + return typeKey(unwrapped); + } + + private PbsAst.TypeRef unwrapGroup(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return null; + } + if (typeRef.kind() != PbsAst.TypeRefKind.GROUP) { + return typeRef; + } + return unwrapGroup(typeRef.inner()); + } + + private String typeKey(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return "unit"; + } + final var unwrapped = unwrapGroup(typeRef); + if (unwrapped == null) { + return "unit"; + } + return switch (unwrapped.kind()) { + case SIMPLE -> "simple:" + unwrapped.name(); + case SELF -> "self"; + case OPTIONAL -> "optional(" + typeKey(unwrapped.inner()) + ")"; + case UNIT -> "unit"; + case GROUP -> typeKey(unwrapped.inner()); + case NAMED_TUPLE -> "tuple(" + unwrapped.fields().stream() + .map(field -> typeKey(field.typeRef())) + .reduce((left, right) -> left + "," + right) + .orElse("") + ")"; + case ERROR -> "error"; + }; + } + + private static final class NamespaceBinder { + private final NameTable nameTable; + private final DiagnosticSink diagnostics; + + private final Map typeNamespace = new HashMap<>(); + private final Map valueNamespace = new HashMap<>(); + private final Map hostOwnerNamespace = new HashMap<>(); + private final Map callableNamespace = new HashMap<>(); + + private NamespaceBinder(final NameTable nameTable, final DiagnosticSink diagnostics) { + this.nameTable = nameTable; + this.diagnostics = diagnostics; + } + + private void registerType(final String name, final Span span, final String kind) { + registerByNamespace(typeNamespace, name, span, "type namespace", kind); + } + + private void registerValue(final String name, final Span span, final String kind) { + registerByNamespace(valueNamespace, name, span, "value namespace", kind); + } + + @SuppressWarnings("unused") + private void registerHostOwner(final String name, final Span span, final String kind) { + registerByNamespace(hostOwnerNamespace, name, span, "host-owner namespace", kind); + } + + private void registerCallable( + final CallableScope scope, + final String callableName, + final String shape, + final Span span) { + final var callableNameId = nameTable.register(callableName); + final var identity = new CallableIdentity(scope, callableNameId, shape); + final var first = callableNamespace.putIfAbsent(identity, span); + if (first == null) { + return; + } + + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name(), + "Duplicate callable declaration '%s' with shape %s in %s".formatted(callableName, shape, scope.label()), + span, + List.of(new RelatedSpan("First callable declaration is here", first))); + } + + private void registerCtor( + final CallableScope scope, + final String ctorName, + final String shape, + final Span span) { + final var callableNameId = nameTable.register(ctorName); + final var identity = new CallableIdentity(scope, callableNameId, shape); + final var first = callableNamespace.putIfAbsent(identity, span); + if (first == null) { + return; + } + + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name(), + "Duplicate constructor declaration '%s' with shape %s in %s".formatted(ctorName, shape, scope.label()), + span, + List.of(new RelatedSpan("First constructor declaration is here", first))); + } + + private void registerByNamespace( + final Map namespace, + final String declarationName, + final Span span, + final String namespaceName, + final String declarationKind) { + final var nameId = nameTable.register(declarationName); + final var first = namespace.putIfAbsent(nameId, span); + if (first == null) { + return; + } + + diagnostics.report( + Severity.Error, + PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name(), + "Duplicate %s declaration '%s' in %s".formatted(declarationKind, declarationName, namespaceName), + span, + List.of(new RelatedSpan("First declaration is here", first))); + } + } + + private record CallableIdentity( + CallableScope scope, + NameId callableNameId, + String shape) { + } + + private record CallableScope( + String label, + String scopeKind, + NameId ownerNameId) { + + private static CallableScope topLevelFunctions() { + return new CallableScope("top-level callable scope", "top-level", null); + } + + private static CallableScope structMethods(final NameId ownerNameId) { + return new CallableScope("struct method scope", "struct", ownerNameId); + } + + private static CallableScope structCtors(final NameId ownerNameId) { + return new CallableScope("struct constructor scope", "ctor", ownerNameId); + } + + private static CallableScope serviceMethods(final NameId ownerNameId) { + return new CallableScope("service method scope", "service", ownerNameId); + } + + private static CallableScope contractSignatures(final NameId ownerNameId) { + return new CallableScope("contract signature scope", "contract", ownerNameId); + } + + private static CallableScope implementsMethods(final NameId ownerNameId) { + return new CallableScope("implements method scope", "implements", ownerNameId); + } + } +} 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 new file mode 100644 index 00000000..c95be56b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java @@ -0,0 +1,16 @@ +package p.studio.compiler.pbs.semantics; + +public enum PbsSemanticsErrors { + E_SEM_DUPLICATE_DECLARATION, + E_SEM_DUPLICATE_CALLABLE_SHAPE, + E_SEM_DUPLICATE_PARAMETER_NAME, + E_SEM_DUPLICATE_RETURN_LABEL, + E_SEM_DUPLICATE_ERROR_CASE_LABEL, + E_SEM_DUPLICATE_ENUM_CASE_LABEL, + E_SEM_DUPLICATE_ENUM_CASE_ID, + E_SEM_INVALID_MIXED_OPTIONAL_RESULT_RETURN, + E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE, + E_SEM_MISSING_CONST_TYPE_ANNOTATION, + E_SEM_MISSING_CONST_INITIALIZER, + E_SEM_INVALID_RETURN_INSIDE_CTOR, +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java index ba712507..d29a7183 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsFrontendCompilerTest.java @@ -1,6 +1,7 @@ package p.studio.compiler.pbs; import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.semantics.PbsSemanticsErrors; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; @@ -45,6 +46,22 @@ class PbsFrontendCompilerTest { final var compiler = new PbsFrontendCompiler(); compiler.compileFile(new FileId(0), source, diagnostics); - assertTrue(diagnostics.hasErrors()); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name()))); + } + + @Test + void shouldAllowOverloadedFunctionNamesWhenShapeDiffers() { + final var source = """ + fn a(x: int) -> int { return x; } + fn a(x: float) -> int { return 0; } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var compiler = new PbsFrontendCompiler(); + compiler.compileFile(new FileId(0), source, diagnostics); + + assertTrue(diagnostics.stream().noneMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name()))); } } 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 new file mode 100644 index 00000000..6af060cc --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsDeclarationsTest.java @@ -0,0 +1,147 @@ +package p.studio.compiler.pbs.semantics; + +import org.junit.jupiter.api.Test; +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 PbsSemanticsDeclarationsTest { + + @Test + void shouldAllowFunctionOverloadsWithDifferentShapes() { + final var source = """ + fn run(x: int) -> int { return x; } + fn run(x: float) -> int { return 0; } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name()))); + } + + @Test + void shouldRejectDuplicateCallableShapeIgnoringLabels() { + final var source = """ + fn run(a: int) -> (x: int, y: int) { return 0; } + fn run(b: int) -> (left: int, right: int) { return 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_DUPLICATE_CALLABLE_SHAPE.name()))); + } + + @Test + void shouldRejectDuplicateParametersAndReturnLabels() { + final var source = """ + fn dup(a: int, a: int) -> (x: int, x: int) { return 0; } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_PARAMETER_NAME.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RETURN_LABEL.name()))); + } + + @Test + void shouldRejectDuplicateEnumAndErrorCases() { + final var source = """ + declare error Err { Fail; Fail; } + declare enum Mode(Idle = 0, Idle = 1, Run = 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_DUPLICATE_ERROR_CASE_LABEL.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_LABEL.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_ENUM_CASE_ID.name()))); + } + + @Test + void shouldRejectInvalidReturnSurfaces() { + final var source = """ + declare error Err { Fail; } + fn badResult() -> result optional int { return ok(1); } + fn badOptional(v: optional void) -> int { return 0; } + """; + 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_MIXED_OPTIONAL_RESULT_RETURN.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_OPTIONAL_VOID_TYPE_SURFACE.name()))); + } + + @Test + void shouldRejectConstWithoutInitializer() { + final var source = "declare const LIMIT: int;"; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_MISSING_CONST_INITIALIZER.name()))); + } + + @Test + void shouldRejectInvalidCtorServiceAndCallbackShapes() { + final var source = """ + declare struct S(a: int) { + ctor make(x: int) { return; } + ctor make(y: int) { } + } + + declare service Game { + fn run(a: int) -> int { return a; } + fn run(b: int) -> int { return b; } + } + + declare callback Tick(a: int, a: int) -> int; + """; + 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_RETURN_INSIDE_CTOR.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_CALLABLE_SHAPE.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_PARAMETER_NAME.name()))); + } + + @Test + void shouldRejectDuplicateDeclarationNamesByNamespace() { + final var source = """ + declare struct State(); + declare enum State(Idle); + + declare const Tick: int = 1; + declare service Tick { + fn run() -> void { return; } + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_DECLARATION.name()))); + } +}