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 453236ee..19f50685 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 @@ -6,6 +6,7 @@ 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.pbs.semantics.PbsFlowSemanticsValidator; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; import p.studio.utilities.structures.ReadOnlyList; @@ -14,6 +15,7 @@ import java.util.ArrayList; public final class PbsFrontendCompiler { private final PbsDeclarationSemanticsValidator declarationSemanticsValidator = new PbsDeclarationSemanticsValidator(); + private final PbsFlowSemanticsValidator flowSemanticsValidator = new PbsFlowSemanticsValidator(); public IRBackendFile compileFile( final FileId fileId, @@ -29,6 +31,7 @@ public final class PbsFrontendCompiler { final PbsAst.File ast, final DiagnosticSink diagnostics) { declarationSemanticsValidator.validate(ast, diagnostics); + flowSemanticsValidator.validate(ast, diagnostics); return new IRBackendFile(fileId, lowerFunctions(fileId, ast)); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java new file mode 100644 index 00000000..d293be0f --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java @@ -0,0 +1,1831 @@ +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.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 PbsFlowSemanticsValidator { + public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { + final var model = Model.from(ast); + + for (final var topDecl : ast.topDecls()) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + validateCallableBody( + functionDecl.parameters(), + functionDecl.body(), + callableReturnType(functionDecl.returnKind(), functionDecl.returnType(), functionDecl.resultErrorType(), model), + functionDecl.resultErrorType() == null ? null : functionDecl.resultErrorType().name(), + null, + model, + diagnostics); + continue; + } + if (topDecl instanceof PbsAst.StructDecl structDecl) { + final var receiverType = TypeView.struct(structDecl.name()); + for (final var method : structDecl.methods()) { + validateCallableBody( + method.parameters(), + method.body(), + callableReturnType(method.returnKind(), method.returnType(), method.resultErrorType(), model), + method.resultErrorType() == null ? null : method.resultErrorType().name(), + receiverType, + model, + diagnostics); + } + for (final var ctor : structDecl.ctors()) { + validateCallableBody( + ctor.parameters(), + ctor.body(), + TypeView.unit(), + null, + receiverType, + model, + diagnostics); + } + continue; + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + final var receiverType = TypeView.service(serviceDecl.name()); + for (final var method : serviceDecl.methods()) { + validateCallableBody( + method.parameters(), + method.body(), + callableReturnType(method.returnKind(), method.returnType(), method.resultErrorType(), model), + method.resultErrorType() == null ? null : method.resultErrorType().name(), + receiverType, + model, + diagnostics); + } + } + } + } + + private void validateCallableBody( + final ReadOnlyList parameters, + final PbsAst.Block body, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics) { + final var scope = new Scope(); + for (final var parameter : parameters) { + scope.bind(parameter.name(), typeFromTypeRef(parameter.typeRef(), model, receiverType)); + } + analyzeBlock(body, scope, returnType, resultErrorName, receiverType, model, diagnostics, true); + } + + private TypeView analyzeBlock( + final PbsAst.Block block, + final Scope outerScope, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics, + final boolean valueContext) { + final var scope = outerScope.copy(); + for (final var statement : block.statements()) { + analyzeStatement(statement, scope, returnType, resultErrorName, receiverType, model, diagnostics); + } + if (block.tailExpression() == null) { + return TypeView.unit(); + } + return analyzeExpression( + block.tailExpression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + valueContext).type(); + } + + private void analyzeStatement( + final PbsAst.Statement statement, + final Scope scope, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics) { + if (statement instanceof PbsAst.LetStatement letStatement) { + final var expected = letStatement.explicitType() == null + ? null + : typeFromTypeRef(letStatement.explicitType(), model, receiverType); + final var initializer = analyzeExpression( + letStatement.initializer(), + scope, + expected, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true); + scope.bind(letStatement.name(), expected == null ? initializer.type() : expected); + return; + } + if (statement instanceof PbsAst.ReturnStatement returnStatement) { + if (returnStatement.value() != null) { + analyzeExpression( + returnStatement.value(), + scope, + returnType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true); + } + return; + } + if (statement instanceof PbsAst.IfStatement ifStatement) { + final var condition = analyzeExpression( + ifStatement.condition(), + scope, + TypeView.bool(), + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (!isBool(condition)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_IF_NON_BOOL_CONDITION.name(), + "If statement condition must have bool type", + ifStatement.condition().span()); + } + analyzeBlock(ifStatement.thenBlock(), scope, returnType, resultErrorName, receiverType, model, diagnostics, false); + if (ifStatement.elseBlock() != null) { + analyzeBlock(ifStatement.elseBlock(), scope, returnType, resultErrorName, receiverType, model, diagnostics, false); + } + if (ifStatement.elseIf() != null) { + analyzeStatement(ifStatement.elseIf(), scope, returnType, resultErrorName, receiverType, model, diagnostics); + } + return; + } + if (statement instanceof PbsAst.ForStatement forStatement) { + final var iteratorType = typeFromTypeRef(forStatement.iteratorType(), model, receiverType); + final var fromType = analyzeExpression( + forStatement.fromExpression(), + scope, + iteratorType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + final var untilType = analyzeExpression( + forStatement.untilExpression(), + scope, + iteratorType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (!compatible(fromType, iteratorType) || !compatible(untilType, iteratorType)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_FOR_TYPE_MISMATCH.name(), + "For-loop bounds must match declared iterator type", + forStatement.span()); + } + if (forStatement.stepExpression() != null) { + final var stepType = analyzeExpression( + forStatement.stepExpression(), + scope, + iteratorType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (!compatible(stepType, iteratorType)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_FOR_TYPE_MISMATCH.name(), + "For-loop step must match declared iterator type", + forStatement.stepExpression().span()); + } + } + final var bodyScope = scope.copy(); + bodyScope.bind(forStatement.iteratorName(), iteratorType); + analyzeBlock(forStatement.body(), bodyScope, returnType, resultErrorName, receiverType, model, diagnostics, false); + return; + } + if (statement instanceof PbsAst.WhileStatement whileStatement) { + final var condition = analyzeExpression( + whileStatement.condition(), + scope, + TypeView.bool(), + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (!isBool(condition)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_WHILE_NON_BOOL_CONDITION.name(), + "While condition must have bool type", + whileStatement.condition().span()); + } + analyzeBlock(whileStatement.body(), scope, returnType, resultErrorName, receiverType, model, diagnostics, false); + return; + } + if (statement instanceof PbsAst.ExpressionStatement expressionStatement) { + analyzeExpression( + expressionStatement.expression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + false); + return; + } + if (statement instanceof PbsAst.AssignStatement assignStatement) { + analyzeExpression( + assignStatement.value(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true); + } + } + + private ExprResult analyzeExpression( + final PbsAst.Expression expression, + final Scope scope, + final TypeView expectedType, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics, + final ExprUse use, + final boolean valueContext) { + if (expression instanceof PbsAst.IntLiteralExpr) { + return ExprResult.type(TypeView.intType()); + } + if (expression instanceof PbsAst.FloatLiteralExpr) { + return ExprResult.type(TypeView.floatType()); + } + if (expression instanceof PbsAst.BoundedLiteralExpr) { + return ExprResult.type(TypeView.intType()); + } + if (expression instanceof PbsAst.StringLiteralExpr) { + return ExprResult.type(TypeView.str()); + } + if (expression instanceof PbsAst.BoolLiteralExpr) { + return ExprResult.type(TypeView.bool()); + } + if (expression instanceof PbsAst.UnitExpr) { + return ExprResult.type(TypeView.unit()); + } + if (expression instanceof PbsAst.ThisExpr) { + return ExprResult.type(receiverType == null ? TypeView.unknown() : receiverType); + } + if (expression instanceof PbsAst.IdentifierExpr identifierExpr) { + final var localType = scope.resolve(identifierExpr.name()); + if (localType != null) { + return ExprResult.type(localType); + } + + final var serviceType = model.serviceSingletons.get(identifierExpr.name()); + if (serviceType != null) { + return ExprResult.type(serviceType); + } + + final var constType = model.constTypes.get(identifierExpr.name()); + if (constType != null) { + return ExprResult.type(constType); + } + + final var callbackSignature = model.callbacks.get(identifierExpr.name()); + if (callbackSignature != null) { + return ExprResult.type(TypeView.callback(identifierExpr.name(), callbackSignature.inputTypes(), callbackSignature.outputType())); + } + + final var callables = model.topLevelCallables.get(identifierExpr.name()); + if (callables != null && !callables.isEmpty()) { + return ExprResult.callables(callables, false); + } + + if (model.enums.containsKey(identifierExpr.name())) { + return ExprResult.type(TypeView.typeRef(identifierExpr.name())); + } + + return ExprResult.type(TypeView.unknown()); + } + if (expression instanceof PbsAst.GroupExpr groupExpr) { + return analyzeExpression( + groupExpr.expression(), + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + use, + valueContext); + } + if (expression instanceof PbsAst.TupleExpr tupleExpr) { + final var fields = new ArrayList(tupleExpr.items().size()); + for (final var item : tupleExpr.items()) { + final var value = analyzeExpression( + item.expression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + fields.add(new TupleField(item.label(), value)); + } + return ExprResult.type(TypeView.tuple(fields)); + } + if (expression instanceof PbsAst.BlockExpr blockExpr) { + return ExprResult.type(analyzeBlock( + blockExpr.block(), + scope, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + valueContext)); + } + if (expression instanceof PbsAst.UnaryExpr unaryExpr) { + final var operand = analyzeExpression( + unaryExpr.expression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if ("!".equals(unaryExpr.operator()) || "not".equals(unaryExpr.operator())) { + return ExprResult.type(TypeView.bool()); + } + if ("-".equals(unaryExpr.operator()) && (isInt(operand) || isFloat(operand))) { + return ExprResult.type(operand); + } + return ExprResult.type(TypeView.unknown()); + } + if (expression instanceof PbsAst.BinaryExpr binaryExpr) { + final var left = analyzeExpression( + binaryExpr.left(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + final var right = analyzeExpression( + binaryExpr.right(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + return ExprResult.type(inferBinaryResult(binaryExpr.operator(), left, right)); + } + if (expression instanceof PbsAst.MemberExpr memberExpr) { + return analyzeMemberExpression( + memberExpr, + scope, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + use); + } + if (expression instanceof PbsAst.CallExpr callExpr) { + return analyzeCallExpression( + callExpr, + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics); + } + if (expression instanceof PbsAst.ApplyExpr applyExpr) { + return analyzeApplyExpression( + applyExpr, + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics); + } + if (expression instanceof PbsAst.IfExpr ifExpr) { + final var condition = analyzeExpression( + ifExpr.condition(), + scope, + TypeView.bool(), + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (!isBool(condition)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_IF_NON_BOOL_CONDITION.name(), + "If expression condition must have bool type", + ifExpr.condition().span()); + } + + final var thenType = analyzeBlock(ifExpr.thenBlock(), scope, returnType, resultErrorName, receiverType, model, diagnostics, true); + final var elseType = analyzeExpression( + ifExpr.elseExpression(), + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + + if (!compatible(thenType, elseType)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_IF_BRANCH_TYPE_MISMATCH.name(), + "If expression branches must have compatible types", + ifExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + return ExprResult.type(thenType); + } + if (expression instanceof PbsAst.SwitchExpr switchExpr) { + return ExprResult.type(analyzeSwitchExpression( + switchExpr, + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + valueContext)); + } + if (expression instanceof PbsAst.ElseExpr elseExpr) { + final var optional = analyzeExpression( + elseExpr.optionalExpression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (optional.kind() != Kind.OPTIONAL) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_ELSE_NON_OPTIONAL_LEFT.name(), + "Left operand of 'else' must have optional type", + elseExpr.optionalExpression().span()); + analyzeExpression( + elseExpr.fallbackExpression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true); + return ExprResult.type(TypeView.unknown()); + } + + final var payload = optional.inner(); + final var fallback = analyzeExpression( + elseExpr.fallbackExpression(), + scope, + payload, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (!compatible(fallback, payload)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_ELSE_FALLBACK_TYPE_MISMATCH.name(), + "Fallback expression in 'else' must match optional payload type", + elseExpr.fallbackExpression().span()); + } + return ExprResult.type(payload); + } + if (expression instanceof PbsAst.PropagateExpr propagateExpr) { + final var source = analyzeExpression( + propagateExpr.expression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (source.kind() != Kind.RESULT) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_RESULT_PROPAGATE_NON_RESULT.name(), + "Propagation operator '!' requires result type", + propagateExpr.expression().span()); + return ExprResult.type(TypeView.unknown()); + } + final var sourceError = source.errorType(); + if (resultErrorName == null || sourceError == null || !resultErrorName.equals(sourceError.name())) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_RESULT_PROPAGATE_ERROR_MISMATCH.name(), + "Propagation operator '!' requires matching result error type in enclosing callable", + propagateExpr.span()); + } + return ExprResult.type(source.inner()); + } + if (expression instanceof PbsAst.HandleExpr handleExpr) { + return ExprResult.type(analyzeHandleExpression( + handleExpr, + scope, + returnType, + resultErrorName, + receiverType, + model, + diagnostics)); + } + if (expression instanceof PbsAst.NoneExpr noneExpr) { + if (expectedType == null || expectedType.kind() != Kind.OPTIONAL) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_NONE_WITHOUT_EXPECTED_OPTIONAL.name(), + "'none' requires an expected optional type context", + noneExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + return ExprResult.type(expectedType); + } + if (expression instanceof PbsAst.SomeExpr someExpr) { + final var payloadExpected = expectedType != null && expectedType.kind() == Kind.OPTIONAL + ? expectedType.inner() + : null; + final var payload = analyzeExpression( + someExpr.value(), + scope, + payloadExpected, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + return ExprResult.type(TypeView.optional(payloadExpected == null ? payload : payloadExpected)); + } + if (expression instanceof PbsAst.BindExpr bindExpr) { + final var contextType = analyzeExpression( + bindExpr.contextExpression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (expectedType == null || expectedType.kind() != Kind.CALLBACK) { + return ExprResult.type(TypeView.unknown()); + } + + final var topLevel = model.topLevelCallables.get(bindExpr.functionName()); + if (topLevel == null || topLevel.isEmpty()) { + return ExprResult.type(expectedType); + } + final var compatible = new ArrayList(); + for (final var candidate : topLevel) { + if (candidate.inputTypes().size() != expectedType.callbackInputs().size() + 1) { + continue; + } + if (!compatible(contextType, candidate.inputTypes().getFirst())) { + continue; + } + var ok = true; + for (int i = 0; i < expectedType.callbackInputs().size(); i++) { + if (!compatible(expectedType.callbackInputs().get(i), candidate.inputTypes().get(i + 1))) { + ok = false; + break; + } + } + if (!ok) { + continue; + } + if (!compatible(candidate.outputType(), expectedType.callbackOutput())) { + continue; + } + compatible.add(candidate); + } + if (compatible.size() > 1) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_APPLY_AMBIGUOUS_OVERLOAD.name(), + "Bind target resolves ambiguously for callback type", + bindExpr.span()); + } else if (compatible.isEmpty()) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_APPLY_UNRESOLVED_OVERLOAD.name(), + "Bind target does not resolve for callback type", + bindExpr.span()); + } + return ExprResult.type(expectedType); + } + if (expression instanceof PbsAst.NewExpr newExpr) { + if (model.structs.containsKey(newExpr.typeName())) { + return ExprResult.type(TypeView.struct(newExpr.typeName())); + } + return ExprResult.type(TypeView.unknown()); + } + if (expression instanceof PbsAst.AsExpr asExpr) { + analyzeExpression( + asExpr.expression(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true); + if (model.contracts.containsKey(asExpr.contractName())) { + return ExprResult.type(TypeView.contract(asExpr.contractName())); + } + return ExprResult.type(TypeView.unknown()); + } + + if (expression instanceof PbsAst.OkExpr okExpr) { + analyzeExpression( + okExpr.value(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true); + return ExprResult.type(TypeView.unknown()); + } + if (expression instanceof PbsAst.ErrExpr errExpr) { + return ExprResult.type(TypeView.unknown()); + } + + return ExprResult.type(TypeView.unknown()); + } + + private ExprResult analyzeCallExpression( + final PbsAst.CallExpr callExpr, + final Scope scope, + final TypeView expectedType, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics) { + final var callee = analyzeExpression( + callExpr.callee(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.CALL_TARGET, + true); + + final TypeView argumentType; + if (callExpr.arguments().isEmpty()) { + argumentType = TypeView.unit(); + } else if (callExpr.arguments().size() == 1) { + argumentType = analyzeExpression( + callExpr.arguments().getFirst(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + } else { + final var fields = new ArrayList(callExpr.arguments().size()); + for (final var argument : callExpr.arguments()) { + final var itemType = analyzeExpression( + argument, + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + fields.add(new TupleField(null, itemType)); + } + argumentType = TypeView.tuple(fields); + } + + return resolveCallableApplication( + callExpr.span(), + callExpr.callee().span(), + callee, + argumentType, + expectedType, + diagnostics); + } + + private ExprResult analyzeApplyExpression( + final PbsAst.ApplyExpr applyExpr, + final Scope scope, + final TypeView expectedType, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics) { + final var callee = analyzeExpression( + applyExpr.callee(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.APPLY_TARGET, + true); + final var argument = analyzeExpression( + applyExpr.argument(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + + return resolveCallableApplication( + applyExpr.span(), + applyExpr.callee().span(), + callee, + argument, + expectedType, + diagnostics); + } + + private ExprResult resolveCallableApplication( + final Span wholeSpan, + final Span calleeSpan, + final ExprResult callee, + final TypeView argumentType, + final TypeView expectedType, + final DiagnosticSink diagnostics) { + final var candidates = callee.callables(); + if (candidates.isEmpty()) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_APPLY_NON_CALLABLE_TARGET.name(), + "Apply/call target is not callable", + calleeSpan); + return ExprResult.type(TypeView.unknown()); + } + + final var compatible = new ArrayList(); + for (final var candidate : candidates) { + if (inputCompatible(candidate.inputTypes(), argumentType)) { + compatible.add(candidate); + } + } + + if (compatible.isEmpty()) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_APPLY_UNRESOLVED_OVERLOAD.name(), + "No callable overload matches the provided argument", + wholeSpan); + return ExprResult.type(TypeView.unknown()); + } + + if (compatible.size() > 1 && expectedType != null) { + final var narrowed = compatible.stream() + .filter(candidate -> compatible(candidate.outputType(), expectedType)) + .toList(); + if (narrowed.size() == 1) { + return ExprResult.type(narrowed.getFirst().outputType()); + } + if (!narrowed.isEmpty()) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_APPLY_AMBIGUOUS_OVERLOAD.name(), + "Callable overload resolution remains ambiguous", + wholeSpan); + return ExprResult.type(TypeView.unknown()); + } + } + + if (compatible.size() > 1) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_APPLY_AMBIGUOUS_OVERLOAD.name(), + "Callable overload resolution is ambiguous", + wholeSpan); + return ExprResult.type(TypeView.unknown()); + } + + return ExprResult.type(compatible.getFirst().outputType()); + } + + private boolean inputCompatible(final List inputTypes, final TypeView argumentType) { + if (inputTypes.isEmpty()) { + return isUnit(argumentType) || argumentType.kind() == Kind.UNKNOWN; + } + if (inputTypes.size() == 1) { + return compatible(argumentType, inputTypes.getFirst()); + } + if (argumentType.kind() != Kind.TUPLE) { + return false; + } + if (argumentType.tupleFields().size() != inputTypes.size()) { + return false; + } + for (int i = 0; i < inputTypes.size(); i++) { + if (!compatible(argumentType.tupleFields().get(i).type(), inputTypes.get(i))) { + return false; + } + } + return true; + } + + private ExprResult analyzeMemberExpression( + final PbsAst.MemberExpr memberExpr, + final Scope scope, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics, + final ExprUse use) { + final var receiver = analyzeExpression( + memberExpr.receiver(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + + if (receiver.kind() == Kind.TYPE_REF) { + final var enumCases = model.enums.get(receiver.name()); + if (enumCases != null && enumCases.contains(memberExpr.memberName())) { + return ExprResult.type(TypeView.enumType(receiver.name())); + } + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Invalid type member access", + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + + if (receiver.kind() == Kind.TUPLE) { + if (receiver.tupleFields().size() <= 1) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Member projection is not defined for single-slot carrier values", + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + for (final var field : receiver.tupleFields()) { + if (memberExpr.memberName().equals(field.label())) { + return ExprResult.type(field.type()); + } + } + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Tuple label '%s' does not exist".formatted(memberExpr.memberName()), + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + + if (receiver.kind() == Kind.STRUCT) { + final var struct = model.structs.get(receiver.name()); + if (struct == null) { + return ExprResult.type(TypeView.unknown()); + } + final var fieldType = struct.fields().get(memberExpr.memberName()); + if (fieldType != null) { + return ExprResult.type(fieldType); + } + final var methods = struct.methods().get(memberExpr.memberName()); + if (methods != null && !methods.isEmpty()) { + if (use == ExprUse.VALUE) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name(), + "Bare method extraction is not allowed in PBS core", + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + return ExprResult.callables(methods, true); + } + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Struct member '%s' does not exist".formatted(memberExpr.memberName()), + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + + if (receiver.kind() == Kind.SERVICE) { + final var service = model.services.get(receiver.name()); + if (service == null) { + return ExprResult.type(TypeView.unknown()); + } + final var methods = service.methods().get(memberExpr.memberName()); + if (methods != null && !methods.isEmpty()) { + if (use == ExprUse.VALUE) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name(), + "Bare method extraction is not allowed in PBS core", + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + return ExprResult.callables(methods, true); + } + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Service member '%s' does not exist".formatted(memberExpr.memberName()), + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + + if (receiver.kind() == Kind.CONTRACT) { + final var contract = model.contracts.get(receiver.name()); + if (contract == null) { + return ExprResult.type(TypeView.unknown()); + } + final var methods = contract.methods().get(memberExpr.memberName()); + if (methods != null && !methods.isEmpty()) { + if (use == ExprUse.VALUE) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name(), + "Bare method extraction is not allowed in PBS core", + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + return ExprResult.callables(methods, true); + } + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Contract member '%s' does not exist".formatted(memberExpr.memberName()), + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + + diagnostics.error( + PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(), + "Invalid member access target", + memberExpr.span()); + return ExprResult.type(TypeView.unknown()); + } + + private TypeView analyzeSwitchExpression( + final PbsAst.SwitchExpr switchExpr, + final Scope scope, + final TypeView expectedType, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics, + final boolean valueContext) { + final var selector = analyzeExpression( + switchExpr.selector(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + + final var selectorComparable = isScalarComparable(selector) || selector.kind() == Kind.ENUM; + if (!selectorComparable) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_SWITCH_SELECTOR_INVALID.name(), + "Switch selector type is not supported", + switchExpr.selector().span()); + } + + final var seenPatterns = new HashSet(); + boolean hasWildcard = false; + TypeView armType = null; + + for (final var arm : switchExpr.arms()) { + final var patternKey = switchPatternKey(arm.pattern()); + if (arm.pattern() instanceof PbsAst.WildcardSwitchPattern) { + hasWildcard = true; + } else if (!seenPatterns.add(patternKey)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_SWITCH_DUPLICATE_PATTERN.name(), + "Duplicate switch pattern", + arm.pattern().span()); + } + + final var patternType = switchPatternType(arm.pattern(), model, diagnostics); + if (patternType != null && !compatible(patternType, selector)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_SWITCH_PATTERN_TYPE_MISMATCH.name(), + "Switch pattern is not compatible with selector type", + arm.pattern().span()); + } + + final var currentArmType = analyzeBlock( + arm.block(), + scope, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + true); + if (armType == null) { + armType = currentArmType; + } else if (!compatible(currentArmType, armType)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_SWITCH_ARM_TYPE_MISMATCH.name(), + "Switch arm block types must be compatible", + arm.span()); + } + } + + if (valueContext && !hasWildcard) { + var exhaustive = false; + if (selector.kind() == Kind.ENUM) { + final var allCases = model.enums.get(selector.name()); + if (allCases != null) { + final var covered = new HashSet(); + for (final var arm : switchExpr.arms()) { + if (arm.pattern() instanceof PbsAst.EnumCaseSwitchPattern enumCasePattern + && enumCasePattern.path().segments().size() == 2 + && selector.name().equals(enumCasePattern.path().segments().getFirst())) { + covered.add(enumCasePattern.path().segments().get(1)); + } + } + exhaustive = covered.containsAll(allCases); + } + } + if (!exhaustive) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_SWITCH_NON_EXHAUSTIVE.name(), + "Switch expression in value position must be exhaustive", + switchExpr.span()); + } + } + + if (armType == null) { + return TypeView.unit(); + } + if (expectedType != null && compatible(armType, expectedType)) { + return expectedType; + } + return armType; + } + + private String switchPatternKey(final PbsAst.SwitchPattern pattern) { + if (pattern instanceof PbsAst.WildcardSwitchPattern) { + return ""; + } + if (pattern instanceof PbsAst.EnumCaseSwitchPattern enumCaseSwitchPattern) { + return "enum:" + String.join(".", enumCaseSwitchPattern.path().segments().asList()); + } + if (pattern instanceof PbsAst.LiteralSwitchPattern literalSwitchPattern) { + return "lit:" + literalSwitchPattern.literal().toString(); + } + return ""; + } + + private TypeView switchPatternType( + final PbsAst.SwitchPattern pattern, + final Model model, + final DiagnosticSink diagnostics) { + if (pattern instanceof PbsAst.WildcardSwitchPattern) { + return null; + } + if (pattern instanceof PbsAst.EnumCaseSwitchPattern enumCaseSwitchPattern) { + final var segments = enumCaseSwitchPattern.path().segments(); + if (segments.size() != 2) { + return TypeView.unknown(); + } + final var enumName = segments.getFirst(); + final var caseName = segments.get(1); + final var enumCases = model.enums.get(enumName); + if (enumCases == null || !enumCases.contains(caseName)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_SWITCH_PATTERN_TYPE_MISMATCH.name(), + "Enum case pattern '%s.%s' does not resolve".formatted(enumName, caseName), + enumCaseSwitchPattern.span()); + return TypeView.unknown(); + } + return TypeView.enumType(enumName); + } + if (pattern instanceof PbsAst.LiteralSwitchPattern literalSwitchPattern) { + final var literal = literalSwitchPattern.literal(); + if (literal instanceof PbsAst.IntLiteralExpr || literal instanceof PbsAst.BoundedLiteralExpr) { + return TypeView.intType(); + } + if (literal instanceof PbsAst.FloatLiteralExpr) { + return TypeView.floatType(); + } + if (literal instanceof PbsAst.BoolLiteralExpr) { + return TypeView.bool(); + } + if (literal instanceof PbsAst.StringLiteralExpr) { + return TypeView.str(); + } + } + return TypeView.unknown(); + } + + private TypeView analyzeHandleExpression( + final PbsAst.HandleExpr handleExpr, + final Scope scope, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model model, + final DiagnosticSink diagnostics) { + final var sourceType = analyzeExpression( + handleExpr.value(), + scope, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true).type(); + if (sourceType.kind() != Kind.RESULT) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_HANDLE_NON_RESULT.name(), + "Handle requires result expression", + handleExpr.value().span()); + return TypeView.unknown(); + } + if (resultErrorName == null) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(), + "Handle requires enclosing callable with result return", + handleExpr.span()); + } + + final var sourceErrorName = sourceType.errorType() == null ? null : sourceType.errorType().name(); + final var sourceCases = sourceErrorName == null ? Set.of() : model.errors.getOrDefault(sourceErrorName, Set.of()); + final var matchedCases = new HashSet(); + var hasWildcard = false; + + for (final var arm : handleExpr.arms()) { + if (arm.pattern() instanceof PbsAst.WildcardHandlePattern) { + hasWildcard = true; + } else if (arm.pattern() instanceof PbsAst.ErrorPathHandlePattern errorPathHandlePattern) { + final var segments = errorPathHandlePattern.path().segments(); + if (segments.size() == 2) { + final var errorName = segments.getFirst(); + final var caseName = segments.get(1); + if (!errorName.equals(sourceErrorName) || !sourceCases.contains(caseName)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(), + "Handle arm pattern does not match source result error type", + arm.pattern().span()); + } else if (!matchedCases.add(caseName)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(), + "Handle arm duplicates same error case pattern", + arm.pattern().span()); + } + } + } + + if (arm.remapTarget() != null) { + if (!matchesTargetError(arm.remapTarget(), resultErrorName, model)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(), + "Handle remap target must match enclosing callable result error type", + arm.remapTarget().span()); + } + continue; + } + analyzeBlock(arm.block(), scope, returnType, resultErrorName, receiverType, model, diagnostics, true); + } + + if (!hasWildcard && !sourceCases.isEmpty() && !matchedCases.containsAll(sourceCases)) { + diagnostics.error( + PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name(), + "Handle mapping is not exhaustive for source result error cases", + handleExpr.span()); + } + + return sourceType.inner() == null ? TypeView.unknown() : sourceType.inner(); + } + + private boolean matchesTargetError( + final PbsAst.ErrorPath path, + final String resultErrorName, + final Model model) { + if (path == null || resultErrorName == null || path.segments().size() != 2) { + return false; + } + final var errorName = path.segments().getFirst(); + final var caseName = path.segments().get(1); + final var targetCases = model.errors.get(errorName); + return resultErrorName.equals(errorName) && targetCases != null && targetCases.contains(caseName); + } + + private TypeView inferBinaryResult(final String operator, final TypeView left, final TypeView right) { + if ("+".equals(operator) || "-".equals(operator) || "*".equals(operator) || "/".equals(operator) || "%".equals(operator)) { + if (isFloat(left) || isFloat(right)) { + return TypeView.floatType(); + } + if (isInt(left) && isInt(right)) { + return TypeView.intType(); + } + return TypeView.unknown(); + } + if ("==".equals(operator) + || "!=".equals(operator) + || "<".equals(operator) + || "<=".equals(operator) + || ">".equals(operator) + || ">=".equals(operator) + || "and".equals(operator) + || "or".equals(operator) + || "&&".equals(operator) + || "||".equals(operator)) { + return TypeView.bool(); + } + return TypeView.unknown(); + } + + private boolean compatible(final TypeView actual, final TypeView expected) { + if (actual == null || expected == null) { + return true; + } + if (actual.kind() == Kind.UNKNOWN || expected.kind() == Kind.UNKNOWN) { + return true; + } + if (actual.kind() != expected.kind()) { + return false; + } + return switch (actual.kind()) { + case UNIT, INT, FLOAT, BOOL, STR -> true; + case STRUCT, SERVICE, CONTRACT, CALLBACK, ENUM, ERROR, TYPE_REF -> actual.name().equals(expected.name()); + case OPTIONAL -> compatible(actual.inner(), expected.inner()); + case RESULT -> compatible(actual.errorType(), expected.errorType()) && compatible(actual.inner(), expected.inner()); + case TUPLE -> tupleCompatible(actual, expected); + case UNKNOWN -> true; + }; + } + + private boolean tupleCompatible(final TypeView actual, final TypeView expected) { + if (actual.tupleFields().size() != expected.tupleFields().size()) { + return false; + } + for (int i = 0; i < actual.tupleFields().size(); i++) { + if (!compatible(actual.tupleFields().get(i).type(), expected.tupleFields().get(i).type())) { + return false; + } + } + return true; + } + + private TypeView callableReturnType( + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final Model model) { + return switch (returnKind) { + case INFERRED_UNIT, EXPLICIT_UNIT -> TypeView.unit(); + case PLAIN -> collapseReturnPayload(typeFromTypeRef(returnType, model, null)); + case RESULT -> TypeView.result( + typeFromTypeRef(resultErrorType, model, null), + collapseReturnPayload(typeFromTypeRef(returnType, model, null))); + }; + } + + private TypeView collapseReturnPayload(final TypeView type) { + if (type == null) { + return TypeView.unit(); + } + if (type.kind() == Kind.TUPLE && type.tupleFields().size() == 1) { + return type.tupleFields().getFirst().type(); + } + return type; + } + + private TypeView typeFromTypeRef( + final PbsAst.TypeRef typeRef, + final Model model, + final TypeView receiverType) { + if (typeRef == null) { + return TypeView.unit(); + } + return switch (typeRef.kind()) { + case UNIT -> TypeView.unit(); + case SELF -> receiverType == null ? TypeView.unknown() : receiverType; + case SIMPLE -> simpleType(typeRef.name(), model); + case OPTIONAL -> TypeView.optional(typeFromTypeRef(typeRef.inner(), model, receiverType)); + case GROUP -> typeFromTypeRef(typeRef.inner(), model, receiverType); + case NAMED_TUPLE -> { + final var fields = new ArrayList(typeRef.fields().size()); + for (final var field : typeRef.fields()) { + fields.add(new TupleField(field.label(), typeFromTypeRef(field.typeRef(), model, receiverType))); + } + yield TypeView.tuple(fields); + } + case ERROR -> TypeView.unknown(); + }; + } + + private TypeView simpleType(final String name, final Model model) { + if ("int".equals(name)) { + return TypeView.intType(); + } + if ("float".equals(name)) { + return TypeView.floatType(); + } + if ("bool".equals(name)) { + return TypeView.bool(); + } + if ("str".equals(name)) { + return TypeView.str(); + } + if (model.structs.containsKey(name)) { + return TypeView.struct(name); + } + if (model.services.containsKey(name)) { + return TypeView.service(name); + } + if (model.contracts.containsKey(name)) { + return TypeView.contract(name); + } + if (model.enums.containsKey(name)) { + return TypeView.enumType(name); + } + if (model.errors.containsKey(name)) { + return TypeView.error(name); + } + final var callbackSignature = model.callbacks.get(name); + if (callbackSignature != null) { + return TypeView.callback(name, callbackSignature.inputTypes(), callbackSignature.outputType()); + } + return TypeView.unknown(); + } + + private boolean isScalarComparable(final TypeView type) { + return isBool(type) || isInt(type) || isFloat(type) || isStr(type); + } + + private boolean isUnit(final TypeView type) { + return type.kind() == Kind.UNIT || type.kind() == Kind.UNKNOWN; + } + + private boolean isBool(final TypeView type) { + return type.kind() == Kind.BOOL || type.kind() == Kind.UNKNOWN; + } + + private boolean isInt(final TypeView type) { + return type.kind() == Kind.INT || type.kind() == Kind.UNKNOWN; + } + + private boolean isFloat(final TypeView type) { + return type.kind() == Kind.FLOAT || type.kind() == Kind.UNKNOWN; + } + + private boolean isStr(final TypeView type) { + return type.kind() == Kind.STR || type.kind() == Kind.UNKNOWN; + } + + private record ExprResult( + TypeView type, + List callables, + boolean methodTarget) { + private static ExprResult type(final TypeView type) { + return new ExprResult(type, List.of(), false); + } + + private static ExprResult callables(final List callables, final boolean methodTarget) { + return new ExprResult(TypeView.unknown(), List.copyOf(callables), methodTarget); + } + } + + private enum ExprUse { + VALUE, + CALL_TARGET, + APPLY_TARGET + } + + private enum Kind { + UNKNOWN, + UNIT, + INT, + FLOAT, + BOOL, + STR, + STRUCT, + SERVICE, + CONTRACT, + CALLBACK, + ENUM, + ERROR, + OPTIONAL, + RESULT, + TUPLE, + TYPE_REF + } + + private record TupleField( + String label, + TypeView type) { + } + + private record TypeView( + Kind kind, + String name, + List tupleFields, + TypeView inner, + TypeView errorType, + List callbackInputs, + TypeView callbackOutput) { + private static TypeView unknown() { + return new TypeView(Kind.UNKNOWN, null, List.of(), null, null, List.of(), null); + } + + private static TypeView unit() { + return new TypeView(Kind.UNIT, "unit", List.of(), null, null, List.of(), null); + } + + private static TypeView intType() { + return new TypeView(Kind.INT, "int", List.of(), null, null, List.of(), null); + } + + private static TypeView floatType() { + return new TypeView(Kind.FLOAT, "float", List.of(), null, null, List.of(), null); + } + + private static TypeView bool() { + return new TypeView(Kind.BOOL, "bool", List.of(), null, null, List.of(), null); + } + + private static TypeView str() { + return new TypeView(Kind.STR, "str", List.of(), null, null, List.of(), null); + } + + private static TypeView struct(final String name) { + return new TypeView(Kind.STRUCT, name, List.of(), null, null, List.of(), null); + } + + private static TypeView service(final String name) { + return new TypeView(Kind.SERVICE, name, List.of(), null, null, List.of(), null); + } + + private static TypeView contract(final String name) { + return new TypeView(Kind.CONTRACT, name, List.of(), null, null, List.of(), null); + } + + private static TypeView callback( + final String name, + final List inputTypes, + final TypeView outputType) { + return new TypeView(Kind.CALLBACK, name, List.of(), null, null, List.copyOf(inputTypes), outputType); + } + + private static TypeView enumType(final String name) { + return new TypeView(Kind.ENUM, name, List.of(), null, null, List.of(), null); + } + + private static TypeView error(final String name) { + return new TypeView(Kind.ERROR, name, List.of(), null, null, List.of(), null); + } + + private static TypeView optional(final TypeView inner) { + return new TypeView(Kind.OPTIONAL, "optional", List.of(), inner, null, List.of(), null); + } + + private static TypeView result(final TypeView error, final TypeView payload) { + return new TypeView(Kind.RESULT, "result", List.of(), payload, error, List.of(), null); + } + + private static TypeView tuple(final List fields) { + return new TypeView(Kind.TUPLE, "tuple", List.copyOf(fields), null, null, List.of(), null); + } + + private static TypeView typeRef(final String name) { + return new TypeView(Kind.TYPE_REF, name, List.of(), null, null, List.of(), null); + } + } + + private record CallableSymbol( + String name, + List inputTypes, + TypeView outputType, + Span span) { + } + + private record CallbackSignature( + List inputTypes, + TypeView outputType) { + } + + private record StructInfo( + Map fields, + Map> methods) { + } + + private record ServiceInfo( + Map> methods) { + } + + private record ContractInfo( + Map> methods) { + } + + private static final class Model { + private final Map> topLevelCallables = new HashMap<>(); + private final Map structs = new HashMap<>(); + private final Map services = new HashMap<>(); + private final Map contracts = new HashMap<>(); + private final Map callbacks = new HashMap<>(); + private final Map> enums = new HashMap<>(); + private final Map> errors = new HashMap<>(); + private final Map constTypes = new HashMap<>(); + private final Map serviceSingletons = new HashMap<>(); + + private static Model from(final PbsAst.File ast) { + final var model = new Model(); + for (final var topDecl : ast.topDecls()) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + final var fields = new HashMap(); + for (final var field : structDecl.fields()) { + fields.put(field.name(), model.typeFrom(field.typeRef())); + } + final var methods = new HashMap>(); + for (final var method : structDecl.methods()) { + methods.computeIfAbsent(method.name(), ignored -> new ArrayList<>()) + .add(model.callableFrom( + method.name(), + method.parameters(), + method.returnKind(), + method.returnType(), + method.resultErrorType(), + method.span())); + } + model.structs.put(structDecl.name(), new StructInfo(fields, methods)); + continue; + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + final var methods = new HashMap>(); + for (final var method : serviceDecl.methods()) { + methods.computeIfAbsent(method.name(), ignored -> new ArrayList<>()) + .add(model.callableFrom( + method.name(), + method.parameters(), + method.returnKind(), + method.returnType(), + method.resultErrorType(), + method.span())); + } + model.services.put(serviceDecl.name(), new ServiceInfo(methods)); + model.serviceSingletons.put(serviceDecl.name(), TypeView.service(serviceDecl.name())); + continue; + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + final var methods = new HashMap>(); + for (final var signature : contractDecl.signatures()) { + methods.computeIfAbsent(signature.name(), ignored -> new ArrayList<>()) + .add(model.callableFrom( + signature.name(), + signature.parameters(), + signature.returnKind(), + signature.returnType(), + signature.resultErrorType(), + signature.span())); + } + model.contracts.put(contractDecl.name(), new ContractInfo(methods)); + continue; + } + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + model.topLevelCallables.computeIfAbsent(functionDecl.name(), ignored -> new ArrayList<>()) + .add(model.callableFrom( + functionDecl.name(), + functionDecl.parameters(), + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + functionDecl.span())); + continue; + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + final var symbol = model.callableFrom( + callbackDecl.name(), + callbackDecl.parameters(), + callbackDecl.returnKind(), + callbackDecl.returnType(), + callbackDecl.resultErrorType(), + callbackDecl.span()); + model.callbacks.put(callbackDecl.name(), new CallbackSignature(symbol.inputTypes(), symbol.outputType())); + continue; + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + final var cases = new HashSet(); + for (final var enumCase : enumDecl.cases()) { + cases.add(enumCase.name()); + } + model.enums.put(enumDecl.name(), cases); + continue; + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + model.errors.put(errorDecl.name(), new HashSet<>(errorDecl.cases().asList())); + continue; + } + if (topDecl instanceof PbsAst.ConstDecl constDecl && constDecl.explicitType() != null) { + model.constTypes.put(constDecl.name(), model.typeFrom(constDecl.explicitType())); + } + } + return model; + } + + private CallableSymbol callableFrom( + final String name, + final ReadOnlyList parameters, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final Span span) { + final var input = new ArrayList(parameters.size()); + for (final var parameter : parameters) { + input.add(typeFrom(parameter.typeRef())); + } + return new CallableSymbol(name, input, callableReturn(returnKind, returnType, resultErrorType), span); + } + + private TypeView callableReturn( + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + return switch (returnKind) { + case INFERRED_UNIT, EXPLICIT_UNIT -> TypeView.unit(); + case PLAIN -> collapse(typeFrom(returnType)); + case RESULT -> TypeView.result(typeFrom(resultErrorType), collapse(typeFrom(returnType))); + }; + } + + private TypeView collapse(final TypeView type) { + if (type.kind() == Kind.TUPLE && type.tupleFields().size() == 1) { + return type.tupleFields().getFirst().type(); + } + return type; + } + + private TypeView typeFrom(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return TypeView.unit(); + } + return switch (typeRef.kind()) { + case UNIT -> TypeView.unit(); + case SELF -> TypeView.unknown(); + case SIMPLE -> { + if ("int".equals(typeRef.name())) { + yield TypeView.intType(); + } + if ("float".equals(typeRef.name())) { + yield TypeView.floatType(); + } + if ("bool".equals(typeRef.name())) { + yield TypeView.bool(); + } + if ("str".equals(typeRef.name())) { + yield TypeView.str(); + } + if (structs.containsKey(typeRef.name())) { + yield TypeView.struct(typeRef.name()); + } + if (services.containsKey(typeRef.name())) { + yield TypeView.service(typeRef.name()); + } + if (contracts.containsKey(typeRef.name())) { + yield TypeView.contract(typeRef.name()); + } + if (enums.containsKey(typeRef.name())) { + yield TypeView.enumType(typeRef.name()); + } + if (errors.containsKey(typeRef.name())) { + yield TypeView.error(typeRef.name()); + } + final var callbackSignature = callbacks.get(typeRef.name()); + if (callbackSignature != null) { + yield TypeView.callback(typeRef.name(), callbackSignature.inputTypes(), callbackSignature.outputType()); + } + yield TypeView.unknown(); + } + case OPTIONAL -> TypeView.optional(typeFrom(typeRef.inner())); + case GROUP -> typeFrom(typeRef.inner()); + case NAMED_TUPLE -> { + final var fields = new ArrayList(typeRef.fields().size()); + for (final var field : typeRef.fields()) { + fields.add(new TupleField(field.label(), typeFrom(field.typeRef()))); + } + yield TypeView.tuple(fields); + } + case ERROR -> TypeView.unknown(); + }; + } + } + + private static final class Scope { + private final Map names = new HashMap<>(); + + private Scope copy() { + final var scope = new Scope(); + scope.names.putAll(names); + return scope; + } + + private void bind(final String name, final TypeView type) { + names.put(name, type); + } + + private TypeView resolve(final String name) { + return names.get(name); + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java index c95be56b..d45e6de7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java @@ -13,4 +13,25 @@ public enum PbsSemanticsErrors { E_SEM_MISSING_CONST_TYPE_ANNOTATION, E_SEM_MISSING_CONST_INITIALIZER, E_SEM_INVALID_RETURN_INSIDE_CTOR, + E_SEM_APPLY_NON_CALLABLE_TARGET, + E_SEM_APPLY_UNRESOLVED_OVERLOAD, + E_SEM_APPLY_AMBIGUOUS_OVERLOAD, + E_SEM_BARE_METHOD_EXTRACTION, + E_SEM_INVALID_MEMBER_ACCESS, + E_SEM_IF_NON_BOOL_CONDITION, + E_SEM_IF_BRANCH_TYPE_MISMATCH, + E_SEM_SWITCH_SELECTOR_INVALID, + E_SEM_SWITCH_PATTERN_TYPE_MISMATCH, + E_SEM_SWITCH_DUPLICATE_PATTERN, + E_SEM_SWITCH_ARM_TYPE_MISMATCH, + E_SEM_SWITCH_NON_EXHAUSTIVE, + E_SEM_WHILE_NON_BOOL_CONDITION, + E_SEM_FOR_TYPE_MISMATCH, + E_SEM_NONE_WITHOUT_EXPECTED_OPTIONAL, + E_SEM_ELSE_NON_OPTIONAL_LEFT, + E_SEM_ELSE_FALLBACK_TYPE_MISMATCH, + E_SEM_RESULT_PROPAGATE_NON_RESULT, + E_SEM_RESULT_PROPAGATE_ERROR_MISMATCH, + E_SEM_HANDLE_NON_RESULT, + E_SEM_HANDLE_ERROR_MISMATCH, } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsApplyResolutionTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsApplyResolutionTest.java new file mode 100644 index 00000000..1333e0a8 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsApplyResolutionTest.java @@ -0,0 +1,37 @@ +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.assertTrue; + +class PbsSemanticsApplyResolutionTest { + + @Test + void shouldDifferentiateNonCallableUnresolvedAndAmbiguousApply() { + final var source = """ + fn over(x: int) -> int { return x; } + fn over(x: float) -> int { return 0; } + fn pair(a: int, b: int) -> int { return a + b; } + + fn demo() -> int { + let a: int = 1 apply (); + let b: int = pair apply 1; + let c: int = over apply missing; + 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_APPLY_NON_CALLABLE_TARGET.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_APPLY_UNRESOLVED_OVERLOAD.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_APPLY_AMBIGUOUS_OVERLOAD.name()))); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsControlFlowTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsControlFlowTest.java new file mode 100644 index 00000000..7b8ee5ae --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsControlFlowTest.java @@ -0,0 +1,42 @@ +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.assertTrue; + +class PbsSemanticsControlFlowTest { + + @Test + void shouldValidateIfSwitchAndProjectionRules() { + final var source = """ + declare enum Mode(Idle, Run); + + declare struct State(v: int) { + fn value() -> int { return this.v; } + } + + fn flow(m: Mode, s: State) -> int { + let a: int = if 1 { 1 } else { 2 }; + let b: int = switch m { Mode.Idle: { 1 } }; + let c: int = s.missing; + let d = s.value; + return a + b + c + d; + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_IF_NON_BOOL_CONDITION.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_SWITCH_NON_EXHAUSTIVE.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_BARE_METHOD_EXTRACTION.name()))); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsOptionalResultTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsOptionalResultTest.java new file mode 100644 index 00000000..b363108a --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsOptionalResultTest.java @@ -0,0 +1,45 @@ +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.assertTrue; + +class PbsSemanticsOptionalResultTest { + + @Test + void shouldValidateOptionalAndResultFlowRules() { + final var source = """ + declare error ErrA { Fail; } + declare error ErrB { Oops; } + + fn make() -> result int { return ok(1); } + + fn badOptional() -> int { + let x = none; + let y: int = 1 else 2; + return x + y; + } + + fn badResult() -> result int { + let a: int = make()!; + let b: int = handle make() { ErrA.Fail -> ErrA.Fail }; + return ok(a + b); + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_NONE_WITHOUT_EXPECTED_OPTIONAL.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_ELSE_NON_OPTIONAL_LEFT.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_PROPAGATE_ERROR_MISMATCH.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_HANDLE_ERROR_MISMATCH.name()))); + } +}