diff --git a/docs/pbs/pull-requests/PR-018-pbs-result-flow-ok-err-handle-rules.md b/docs/pbs/pull-requests/PR-018-pbs-result-flow-ok-err-handle-rules.md deleted file mode 100644 index 92e33d7c..00000000 --- a/docs/pbs/pull-requests/PR-018-pbs-result-flow-ok-err-handle-rules.md +++ /dev/null @@ -1,44 +0,0 @@ -# PR-018 - PBS Result Flow (ok/err/handle) Rule Enforcement - -## Briefing - -Result-flow rules are only partially enforced. This PR enforces allowed positions for `ok/err`, error-label validation, and `handle` arm terminal form requirements. - -## Motivation - -Result-flow constructs are normative control-flow surfaces and must not behave as unconstrained expressions. - -## Target - -- `ok(...)` and `err(...)` validation in return-flow contexts. -- `handle` semantic validation, including terminal form of block arms. - -## Scope - -- Restrict `ok/err` to allowed result-flow positions. -- Validate `err(E.case)` against declared error type. -- In `handle` block arms, enforce terminal `ok(payload)` or `err(E2.case)`. - -## Method - -- Add expression-use/context flags for result-flow forms. -- Add dedicated diagnostics for invalid placement and invalid error labels. -- Add terminal-check pass for handle block arms. - -## Acceptance Criteria - -- `ok/err` outside allowed positions are rejected deterministically. -- `return ok/err` validates declared `result` contract. -- `handle` block arms violating terminal rules are rejected deterministically. -- Existing propagate (`!`) and handle exhaustiveness checks remain stable. - -## Tests - -- Add positive and negative fixtures for `return ok/err`. -- Add invalid-position tests for `ok/err` as ordinary expression. -- Add `handle` arm terminal-form tests. - -## Non-Goals - -- New result syntax. -- Runtime trap policy changes. diff --git a/docs/pbs/pull-requests/PR-019-pbs-non-unit-fallthrough-diagnostics.md b/docs/pbs/pull-requests/PR-019-pbs-non-unit-fallthrough-diagnostics.md deleted file mode 100644 index 79ef8882..00000000 --- a/docs/pbs/pull-requests/PR-019-pbs-non-unit-fallthrough-diagnostics.md +++ /dev/null @@ -1,42 +0,0 @@ -# PR-019 - PBS Non-Unit Fallthrough Diagnostics - -## Briefing - -The frontend does not currently reject fallthrough of plain non-unit functions and `result` functions at end-of-body paths. This PR adds control-flow completion checks. - -## Motivation - -Return-surface obligations are normative and must be statically enforced. - -## Target - -- Callable body completion validation for function/method/ctor contexts. - -## Scope - -- Reject plain non-unit callable bodies that may complete without explicit return. -- Reject `result` callable bodies that may complete without explicit return. -- Preserve existing unit and optional fallthrough behavior. - -## Method - -- Add path-completion analysis over statement/block graph. -- Integrate with existing flow body analyzer. -- Emit deterministic diagnostics with stable code/phase. - -## Acceptance Criteria - -- Possible fallthrough on plain non-unit callable is rejected. -- Possible fallthrough on `result` callable is rejected. -- `void` and `optional` callable fallthrough behavior remains valid. - -## Tests - -- Add positive/negative control-flow completion fixtures. -- Cover nested `if`/`switch`/loop interactions. -- Run full flow semantics suite. - -## Non-Goals - -- Dataflow-based optimization. -- Runtime control-flow instrumentation. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java index 633b2324..3ee7a147 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java @@ -4,10 +4,13 @@ import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.utilities.structures.ReadOnlyList; import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.ExprUse; +import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Kind; import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Model; import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Scope; import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.TypeView; +import java.util.HashSet; + final class PbsFlowBodyAnalyzer { private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps(); private final PbsFlowExpressionAnalyzer expressionAnalyzer = new PbsFlowExpressionAnalyzer(typeOps); @@ -85,6 +88,150 @@ final class PbsFlowBodyAnalyzer { scope.bind(parameter.name(), typeOps.typeFromTypeRef(parameter.typeRef(), model, receiverType)); } analyzeBlock(body, scope, returnType, resultErrorName, receiverType, model, diagnostics, true); + validateCallableCompletion(body, returnType, model, diagnostics); + } + + private void validateCallableCompletion( + final PbsAst.Block body, + final TypeView returnType, + final Model model, + final DiagnosticSink diagnostics) { + final var returnKind = returnType.kind(); + final var enforceResultCompletion = returnKind == Kind.RESULT; + final var enforcePlainNonUnitCompletion = returnKind != Kind.RESULT + && returnKind != Kind.UNIT + && returnKind != Kind.OPTIONAL + && returnKind != Kind.UNKNOWN; + + if (!enforceResultCompletion && !enforcePlainNonUnitCompletion) { + return; + } + if (blockAlwaysReturns(body, model)) { + return; + } + + if (enforceResultCompletion) { + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name(), + "Possible fallthrough in result callable body; all paths must return explicitly", + body.span()); + return; + } + + p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics, + PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_NON_UNIT.name(), + "Possible fallthrough in plain non-unit callable body; all paths must return explicitly", + body.span()); + } + + private boolean blockAlwaysReturns( + final PbsAst.Block block, + final Model model) { + if (block == null) { + return false; + } + + for (final var statement : block.statements()) { + if (statementAlwaysReturns(statement, model)) { + return true; + } + } + return expressionAlwaysReturns(block.tailExpression(), model); + } + + private boolean statementAlwaysReturns( + final PbsAst.Statement statement, + final Model model) { + if (statement instanceof PbsAst.ReturnStatement) { + return true; + } + if (statement instanceof PbsAst.IfStatement ifStatement) { + final var thenReturns = blockAlwaysReturns(ifStatement.thenBlock(), model); + final boolean elseReturns; + if (ifStatement.elseBlock() != null) { + elseReturns = blockAlwaysReturns(ifStatement.elseBlock(), model); + } else { + elseReturns = ifStatement.elseIf() != null && statementAlwaysReturns(ifStatement.elseIf(), model); + } + return thenReturns && elseReturns; + } + if (statement instanceof PbsAst.ExpressionStatement expressionStatement) { + return expressionAlwaysReturns(expressionStatement.expression(), model); + } + return false; + } + + private boolean expressionAlwaysReturns( + final PbsAst.Expression expression, + final Model model) { + if (expression == null) { + return false; + } + if (expression instanceof PbsAst.GroupExpr groupExpr) { + return expressionAlwaysReturns(groupExpr.expression(), model); + } + if (expression instanceof PbsAst.BlockExpr blockExpr) { + return blockAlwaysReturns(blockExpr.block(), model); + } + if (expression instanceof PbsAst.IfExpr ifExpr) { + return blockAlwaysReturns(ifExpr.thenBlock(), model) + && expressionAlwaysReturns(ifExpr.elseExpression(), model); + } + if (expression instanceof PbsAst.SwitchExpr switchExpr) { + return switchAlwaysReturns(switchExpr, model); + } + return false; + } + + private boolean switchAlwaysReturns( + final PbsAst.SwitchExpr switchExpr, + final Model model) { + if (switchExpr.arms().isEmpty()) { + return false; + } + if (!switchIsExhaustive(switchExpr, model)) { + return false; + } + for (final var arm : switchExpr.arms()) { + if (!blockAlwaysReturns(arm.block(), model)) { + return false; + } + } + return true; + } + + private boolean switchIsExhaustive( + final PbsAst.SwitchExpr switchExpr, + final Model model) { + for (final var arm : switchExpr.arms()) { + if (arm.pattern() instanceof PbsAst.WildcardSwitchPattern) { + return true; + } + } + + String enumName = null; + final var coveredCases = new HashSet(); + for (final var arm : switchExpr.arms()) { + if (!(arm.pattern() instanceof PbsAst.EnumCaseSwitchPattern enumCaseSwitchPattern)) { + return false; + } + final var segments = enumCaseSwitchPattern.path().segments(); + if (segments.size() != 2) { + return false; + } + if (enumName == null) { + enumName = segments.getFirst(); + } else if (!enumName.equals(segments.getFirst())) { + return false; + } + coveredCases.add(segments.get(1)); + } + if (enumName == null) { + return false; + } + + final var enumCases = model.enums.get(enumName); + return enumCases != null && coveredCases.containsAll(enumCases); } private TypeView analyzeBlock( 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 d6c3648c..c2446cbd 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 @@ -36,6 +36,8 @@ public enum PbsSemanticsErrors { E_SEM_SWITCH_DUPLICATE_PATTERN, E_SEM_SWITCH_ARM_TYPE_MISMATCH, E_SEM_SWITCH_NON_EXHAUSTIVE, + E_SEM_POSSIBLE_FALLTHROUGH_NON_UNIT, + E_SEM_POSSIBLE_FALLTHROUGH_RESULT, E_SEM_WHILE_NON_BOOL_CONDITION, E_SEM_FOR_TYPE_MISMATCH, E_SEM_NONE_WITHOUT_EXPECTED_OPTIONAL, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsFallthroughTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsFallthroughTest.java new file mode 100644 index 00000000..978217f8 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsSemanticsFallthroughTest.java @@ -0,0 +1,113 @@ +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class PbsSemanticsFallthroughTest { + + @Test + void shouldRejectPossibleFallthroughForPlainNonUnitAndResultCallables() { + final var source = """ + declare error Err { Fail; } + + fn plainIfBad(flag: bool) -> int { + if flag { return 1; } + } + + fn plainLoopBad(flag: bool) -> int { + while flag { + return 1; + } + } + + fn resultBad(flag: bool) -> result int { + if flag { return ok(1); } + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var plainFallthroughCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_NON_UNIT.name())) + .count(); + final var resultFallthroughCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name())) + .count(); + + assertEquals(2, plainFallthroughCount); + assertEquals(1, resultFallthroughCount); + } + + @Test + void shouldAllowFallthroughForUnitAndOptionalCallables() { + final var source = """ + fn unitOk(flag: bool) -> void { + if flag { return; } + } + + fn optionalOk(flag: bool, v: int) -> optional int { + if flag { return some(v); } + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_NON_UNIT.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name()))); + } + + @Test + void shouldAcceptNonFallthroughWhenAllPathsReturn() { + final var source = """ + declare enum Mode(Idle, Run); + declare error Err { Fail; } + + fn plainSwitchWildcardGood(mode: Mode) -> int { + switch mode { + Mode.Idle: { return 1; }, + _: { return 2; }, + }; + } + + fn plainSwitchEnumExhaustiveGood(mode: Mode) -> int { + switch mode { + Mode.Idle: { return 1; }, + Mode.Run: { return 2; }, + }; + } + + fn plainNestedIfGood(flag: bool, mode: Mode) -> int { + if flag { + switch mode { + Mode.Idle: { return 1; }, + _: { return 2; }, + }; + } else { + return 3; + } + } + + fn resultGood(flag: bool) -> result int { + if flag { return ok(1); } + return err(Err.Fail); + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_NON_UNIT.name()))); + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name()))); + } +}