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 new file mode 100644 index 00000000..5ca52dbc --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCallableScope.java @@ -0,0 +1,33 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.source.identifiers.NameId; + +record PbsCallableScope( + String label, + String scopeKind, + NameId ownerNameId) { + + static PbsCallableScope topLevelFunctions() { + return new PbsCallableScope("top-level callable scope", "top-level", null); + } + + static PbsCallableScope structMethods(final NameId ownerNameId) { + return new PbsCallableScope("struct method scope", "struct", ownerNameId); + } + + static PbsCallableScope structCtors(final NameId ownerNameId) { + return new PbsCallableScope("struct constructor scope", "ctor", ownerNameId); + } + + static PbsCallableScope serviceMethods(final NameId ownerNameId) { + return new PbsCallableScope("service method scope", "service", ownerNameId); + } + + static PbsCallableScope contractSignatures(final NameId ownerNameId) { + return new PbsCallableScope("contract signature scope", "contract", 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/PbsCallableShape.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCallableShape.java new file mode 100644 index 00000000..f42854a6 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCallableShape.java @@ -0,0 +1,91 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.utilities.structures.ReadOnlyList; + +final class PbsCallableShape { + private PbsCallableShape() { + } + + static 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(); + } + + static 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 static 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 static 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 static 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 static 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"; + }; + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCtorReturnScanner.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCtorReturnScanner.java new file mode 100644 index 00000000..eb418699 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsCtorReturnScanner.java @@ -0,0 +1,74 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.pbs.ast.PbsAst; + +final class PbsCtorReturnScanner { + private PbsCtorReturnScanner() { + } + + static 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 static 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 static 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; + } +} 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 new file mode 100644 index 00000000..c0ff92a3 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationRuleValidator.java @@ -0,0 +1,187 @@ +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.HashMap; +import java.util.List; + +final class PbsDeclarationRuleValidator { + private final NameTable nameTable; + private final DiagnosticSink diagnostics; + + PbsDeclarationRuleValidator( + final NameTable nameTable, + final DiagnosticSink diagnostics) { + this.nameTable = nameTable; + this.diagnostics = diagnostics; + } + + void validateCallableHeader( + final String callableName, + final ReadOnlyList parameters, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final Span span) { + validateParameters(parameters, "callable '%s'".formatted(callableName)); + validateReturnSurface(returnKind, returnType, resultErrorType, "callable '%s'".formatted(callableName), span); + PbsTypeSurfaceSemanticsValidator.validateTypeSurfaceRecursive( + parameters.stream().map(PbsAst.Parameter::typeRef).toList(), + "callable '%s'".formatted(callableName), + diagnostics); + } + + void validateParameters( + final ReadOnlyList parameters, + final String ownerDescription) { + 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))); + } + } + } + + void validateTypeSurfaces( + final List typeRoots, + final String ownerDescription) { + PbsTypeSurfaceSemanticsValidator.validateTypeSurfaceRecursive(typeRoots, ownerDescription, diagnostics); + } + + void validateErrorDeclaration(final PbsAst.ErrorDecl errorDecl) { + 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))); + } + } + } + + void validateEnumDeclaration(final PbsAst.EnumDecl enumDecl) { + 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))); + } + } + } + } + + void validateCallbackDeclaration(final PbsAst.CallbackDecl callbackDecl) { + validateParameters(callbackDecl.parameters(), "callback '%s'".formatted(callbackDecl.name())); + validateReturnSurface( + callbackDecl.returnKind(), + callbackDecl.returnType(), + callbackDecl.resultErrorType(), + "callback '%s'".formatted(callbackDecl.name()), + callbackDecl.span()); + PbsTypeSurfaceSemanticsValidator.validateTypeSurfaceRecursive( + callbackDecl.parameters().stream().map(PbsAst.Parameter::typeRef).toList(), + "callback '%s'".formatted(callbackDecl.name()), + diagnostics); + } + + void validateConstDeclaration(final PbsAst.ConstDecl constDecl) { + 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 { + PbsTypeSurfaceSemanticsValidator.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 validateReturnSurface( + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final String ownerDescription, + final Span span) { + if (returnKind == PbsAst.ReturnKind.RESULT && PbsTypeSurfaceSemanticsValidator.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); + } + + if (returnType != null) { + PbsTypeSurfaceSemanticsValidator.validateTypeSurfaceRecursive(List.of(returnType), ownerDescription, diagnostics); + } + if (resultErrorType != null) { + PbsTypeSurfaceSemanticsValidator.validateTypeSurfaceRecursive(List.of(resultErrorType), ownerDescription, diagnostics); + } + } + + private void validateDuplicateOutputLabels( + final PbsAst.TypeRef namedTupleReturnType, + final String ownerDescription) { + 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))); + } + } + } +} 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 ec49158a..9f4afdb3 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 @@ -3,27 +3,20 @@ 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); + final var binder = new PbsNamespaceBinder(nameTable, diagnostics); + final var rules = new PbsDeclarationRuleValidator(nameTable, diagnostics); for (final var topDecl : ast.topDecls()) { if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { validateCallable( - CallableScope.topLevelFunctions(), + PbsCallableScope.topLevelFunctions(), functionDecl.name(), functionDecl.parameters(), functionDecl.returnKind(), @@ -31,64 +24,66 @@ public final class PbsDeclarationSemanticsValidator { functionDecl.resultErrorType(), functionDecl.span(), binder, - diagnostics); + rules); continue; } if (topDecl instanceof PbsAst.StructDecl structDecl) { binder.registerType(structDecl.name(), structDecl.span(), "struct"); - validateStructDeclaration(structDecl, binder, diagnostics); + validateStructDeclaration(structDecl, binder, rules, 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); + validateServiceDeclaration(serviceDecl, binder, rules); continue; } if (topDecl instanceof PbsAst.ContractDecl contractDecl) { binder.registerType(contractDecl.name(), contractDecl.span(), "contract"); - validateContractDeclaration(contractDecl, binder, diagnostics); + validateContractDeclaration(contractDecl, binder, rules); continue; } if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { binder.registerType(errorDecl.name(), errorDecl.span(), "error"); - validateErrorDeclaration(errorDecl, diagnostics); + rules.validateErrorDeclaration(errorDecl); continue; } if (topDecl instanceof PbsAst.EnumDecl enumDecl) { binder.registerType(enumDecl.name(), enumDecl.span(), "enum"); - validateEnumDeclaration(enumDecl, diagnostics); + rules.validateEnumDeclaration(enumDecl); continue; } if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { binder.registerType(callbackDecl.name(), callbackDecl.span(), "callback"); - validateCallbackDeclaration(callbackDecl, diagnostics); + rules.validateCallbackDeclaration(callbackDecl); continue; } if (topDecl instanceof PbsAst.ConstDecl constDecl) { binder.registerValue(constDecl.name(), constDecl.span(), "const"); - validateConstDeclaration(constDecl, diagnostics); + rules.validateConstDeclaration(constDecl); continue; } if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) { - validateImplementsDeclaration(implementsDecl, binder, diagnostics); + validateImplementsDeclaration(implementsDecl, binder, rules); } } } private void validateStructDeclaration( final PbsAst.StructDecl structDecl, - final NamespaceBinder binder, + final PbsNamespaceBinder binder, + final PbsDeclarationRuleValidator rules, final DiagnosticSink diagnostics) { - final var methodScope = CallableScope.structMethods(nameTable.register(structDecl.name())); + final var ownerNameId = nameTable.register(structDecl.name()); + final var methodScope = PbsCallableScope.structMethods(ownerNameId); for (final var method : structDecl.methods()) { validateCallable( methodScope, @@ -99,19 +94,18 @@ public final class PbsDeclarationSemanticsValidator { method.resultErrorType(), method.span(), binder, - diagnostics); + rules); } - final var ctorScope = CallableScope.structCtors(nameTable.register(structDecl.name())); + final var ctorScope = PbsCallableScope.structCtors(ownerNameId); for (final var ctor : structDecl.ctors()) { - validateParameters(ctor.parameters(), "ctor '%s'".formatted(ctor.name()), diagnostics); - validateTypeSurfaceRecursive( + rules.validateParameters(ctor.parameters(), "ctor '%s'".formatted(ctor.name())); + rules.validateTypeSurfaces( ctor.parameters().stream().map(PbsAst.Parameter::typeRef).toList(), - "ctor '%s'".formatted(ctor.name()), - diagnostics); + "ctor '%s'".formatted(ctor.name())); - binder.registerCtor(ctorScope, ctor.name(), ctorShapeKey(ctor.parameters()), ctor.span()); - if (containsReturnStatement(ctor.body())) { + binder.registerCtor(ctorScope, ctor.name(), PbsCallableShape.ctorShapeKey(ctor.parameters()), ctor.span()); + if (PbsCtorReturnScanner.containsReturnStatement(ctor.body())) { diagnostics.error( PbsSemanticsErrors.E_SEM_INVALID_RETURN_INSIDE_CTOR.name(), "Constructors cannot contain 'return' statements", @@ -122,9 +116,9 @@ public final class PbsDeclarationSemanticsValidator { private void validateServiceDeclaration( final PbsAst.ServiceDecl serviceDecl, - final NamespaceBinder binder, - final DiagnosticSink diagnostics) { - final var methodScope = CallableScope.serviceMethods(nameTable.register(serviceDecl.name())); + final PbsNamespaceBinder binder, + final PbsDeclarationRuleValidator rules) { + final var methodScope = PbsCallableScope.serviceMethods(nameTable.register(serviceDecl.name())); for (final var method : serviceDecl.methods()) { validateCallable( methodScope, @@ -135,15 +129,15 @@ public final class PbsDeclarationSemanticsValidator { method.resultErrorType(), method.span(), binder, - diagnostics); + rules); } } private void validateContractDeclaration( final PbsAst.ContractDecl contractDecl, - final NamespaceBinder binder, - final DiagnosticSink diagnostics) { - final var signatureScope = CallableScope.contractSignatures(nameTable.register(contractDecl.name())); + final PbsNamespaceBinder binder, + final PbsDeclarationRuleValidator rules) { + final var signatureScope = PbsCallableScope.contractSignatures(nameTable.register(contractDecl.name())); for (final var signature : contractDecl.signatures()) { validateCallable( signatureScope, @@ -154,105 +148,16 @@ public final class PbsDeclarationSemanticsValidator { 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()); + rules); } } private void validateImplementsDeclaration( final PbsAst.ImplementsDecl implementsDecl, - final NamespaceBinder binder, - final DiagnosticSink diagnostics) { + final PbsNamespaceBinder binder, + final PbsDeclarationRuleValidator rules) { final var ownerNameId = nameTable.register(implementsDecl.ownerName() + ":" + implementsDecl.contractName()); - final var scope = CallableScope.implementsMethods(ownerNameId); + final var scope = PbsCallableScope.implementsMethods(ownerNameId); for (final var method : implementsDecl.methods()) { validateCallable( scope, @@ -263,407 +168,25 @@ public final class PbsDeclarationSemanticsValidator { method.resultErrorType(), method.span(), binder, - diagnostics); + rules); } } private void validateCallable( - final CallableScope scope, + final PbsCallableScope 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); - } + final PbsNamespaceBinder binder, + final PbsDeclarationRuleValidator rules) { + rules.validateCallableHeader(callableName, parameters, returnKind, returnType, resultErrorType, span); + binder.registerCallable( + scope, + callableName, + PbsCallableShape.callableShapeKey(parameters, returnKind, returnType, resultErrorType), + span); } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsNamespaceBinder.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsNamespaceBinder.java new file mode 100644 index 00000000..90c13388 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsNamespaceBinder.java @@ -0,0 +1,106 @@ +package p.studio.compiler.pbs.semantics; + +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 java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class PbsNamespaceBinder { + 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<>(); + + PbsNamespaceBinder(final NameTable nameTable, final DiagnosticSink diagnostics) { + this.nameTable = nameTable; + this.diagnostics = diagnostics; + } + + void registerType(final String name, final Span span, final String kind) { + registerByNamespace(typeNamespace, name, span, "type namespace", kind); + } + + void registerValue(final String name, final Span span, final String kind) { + registerByNamespace(valueNamespace, name, span, "value namespace", kind); + } + + @SuppressWarnings("unused") + void registerHostOwner(final String name, final Span span, final String kind) { + registerByNamespace(hostOwnerNamespace, name, span, "host-owner namespace", kind); + } + + void registerCallable( + final PbsCallableScope 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))); + } + + void registerCtor( + final PbsCallableScope 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( + PbsCallableScope scope, + NameId callableNameId, + String shape) { + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsTypeSurfaceSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsTypeSurfaceSemanticsValidator.java new file mode 100644 index 00000000..78c6cc66 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsTypeSurfaceSemanticsValidator.java @@ -0,0 +1,59 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.source.diagnostics.DiagnosticSink; + +import java.util.ArrayList; +import java.util.List; + +final class PbsTypeSurfaceSemanticsValidator { + private PbsTypeSurfaceSemanticsValidator() { + } + + static 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. + } + } + } + } + + static 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; + } +}