From d7b070ab6888a284162bb966e2de253cb28d696e Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 6 Mar 2026 09:35:56 +0000 Subject: [PATCH] implements PR021 --- docs/pbs/pull-requests/INDEX.md | 1 + .../PR-020-pbs-lowering-admission-gates.md | 43 -------- .../pbs/semantics/PbsFlowBodyAnalyzer.java | 48 +++++++++ .../PbsSemanticsFallthroughTest.java | 97 +++++++++++++++++++ 4 files changed, 146 insertions(+), 43 deletions(-) delete mode 100644 docs/pbs/pull-requests/PR-020-pbs-lowering-admission-gates.md diff --git a/docs/pbs/pull-requests/INDEX.md b/docs/pbs/pull-requests/INDEX.md index 17511670..3bf0d172 100644 --- a/docs/pbs/pull-requests/INDEX.md +++ b/docs/pbs/pull-requests/INDEX.md @@ -29,3 +29,4 @@ Cada documento e auto contido e inclui: `Briefing`, `Target`, `Method`, `Accepta 20. `PR-018-pbs-result-flow-ok-err-handle-rules.md` 21. `PR-019-pbs-non-unit-fallthrough-diagnostics.md` 22. `PR-020-pbs-lowering-admission-gates.md` +23. `PR-021-pbs-handle-aware-fallthrough-completion.md` diff --git a/docs/pbs/pull-requests/PR-020-pbs-lowering-admission-gates.md b/docs/pbs/pull-requests/PR-020-pbs-lowering-admission-gates.md deleted file mode 100644 index 93f7bb11..00000000 --- a/docs/pbs/pull-requests/PR-020-pbs-lowering-admission-gates.md +++ /dev/null @@ -1,43 +0,0 @@ -# PR-020 - PBS Lowering Admission Gates - -## Briefing - -Lowering currently proceeds even when syntax/static/linking diagnostics contain errors. This PR introduces explicit admission gates so invalid programs are rejected before IR lowering. - -## Motivation - -Lowering must not convert required rejections into accepted lowered artifacts. - -## Target - -- `PbsFrontendCompiler` and `PBSFrontendPhaseService` lowering flow. -- Frontend-to-IR admission policy. - -## Scope - -- Block IR emission for source units with fatal diagnostics. -- Preserve deterministic diagnostics already emitted by previous phases. -- Keep successful paths unchanged. - -## Method - -- Introduce phase gate checks before `lowerFunctions` / file merge. -- Ensure failed units are excluded from emitted IR. -- Keep build issue adaptation behavior stable. - -## Acceptance Criteria - -- Files/modules with syntax/static/linking errors are not lowered. -- No invalid callable appears in IR output after failed admission. -- Valid files continue lowering unchanged. - -## Tests - -- Add tests proving failed parse/semantic/linking input does not emit IR functions. -- Add tests proving clean input still emits expected IR. -- Run frontend phase service and compiler suites. - -## Non-Goals - -- Redefining diagnostics severity policy. -- Introducing partial lowering recovery strategy. 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 3ee7a147..cef6a753 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 @@ -180,9 +180,57 @@ final class PbsFlowBodyAnalyzer { if (expression instanceof PbsAst.SwitchExpr switchExpr) { return switchAlwaysReturns(switchExpr, model); } + if (expression instanceof PbsAst.HandleExpr handleExpr) { + return handleAlwaysReturns(handleExpr, model); + } return false; } + private boolean handleAlwaysReturns( + final PbsAst.HandleExpr handleExpr, + final Model model) { + if (expressionAlwaysReturns(handleExpr.value(), model)) { + return true; + } + if (handleExpr.arms().isEmpty()) { + return false; + } + for (final var arm : handleExpr.arms()) { + if (!handleArmAlwaysReturns(arm, model)) { + return false; + } + } + return true; + } + + private boolean handleArmAlwaysReturns( + final PbsAst.HandleArm arm, + final Model model) { + if (arm.remapTarget() != null) { + return true; + } + if (arm.block() == null) { + return false; + } + final var terminal = unwrapGroup(arm.block().tailExpression()); + if (terminal instanceof PbsAst.ErrExpr errExpr) { + return isKnownErrorPath(errExpr.errorPath(), model); + } + return false; + } + + private boolean isKnownErrorPath( + final PbsAst.ErrorPath path, + final Model model) { + if (path == null || path.segments().size() != 2) { + return false; + } + final var errorName = path.segments().getFirst(); + final var caseName = path.segments().get(1); + final var errorCases = model.errors.get(errorName); + return errorCases != null && errorCases.contains(caseName); + } + private boolean switchAlwaysReturns( final PbsAst.SwitchExpr switchExpr, final Model model) { 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 index 978217f8..f778b5fe 100644 --- 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 @@ -7,6 +7,7 @@ import p.studio.compiler.source.identifiers.FileId; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; class PbsSemanticsFallthroughTest { @@ -110,4 +111,100 @@ class PbsSemanticsFallthroughTest { assertFalse(diagnostics.stream().anyMatch(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name()))); } + + @Test + void shouldTreatTerminalHandleArmsAsCallableCompletion() { + final var source = """ + declare enum Mode(Idle, Run); + declare error InErr { A; B; } + declare error OutErr { Retry; Abort; } + + fn source(v: int) -> result int { + if v > 0 { return err(InErr.A); } + return err(InErr.B); + } + + fn handleShortRemapTerminal(v: int) -> result int { + handle source(v) { + InErr.A -> OutErr.Retry, + InErr.B -> OutErr.Abort, + }; + } + + fn handleBlockErrTerminal(v: int) -> result int { + handle source(v) { + InErr.A -> { err(OutErr.Retry) }, + InErr.B -> { err(OutErr.Abort) }, + }; + } + + fn handleTerminalInIf(flag: bool, v: int) -> result int { + if flag { + handle source(v) { + InErr.A -> OutErr.Retry, + InErr.B -> OutErr.Abort, + }; + } else { + return err(OutErr.Abort); + } + } + + fn handleTerminalInSwitch(mode: Mode, v: int) -> result int { + switch mode { + Mode.Idle: { + handle source(v) { + InErr.A -> { err(OutErr.Retry) }, + InErr.B -> { err(OutErr.Abort) }, + }; + }, + _: { return err(OutErr.Abort); }, + }; + } + """; + 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_RESULT.name()))); + } + + @Test + void shouldKeepFallthroughWhenHandleArmRecoversWithOk() { + final var source = """ + declare error InErr { A; B; } + declare error OutErr { Retry; Abort; } + + fn source(v: int) -> result int { + if v > 0 { return err(InErr.A); } + return err(InErr.B); + } + + fn handleRecoveringTail(v: int) -> result int { + handle source(v) { + InErr.A -> { ok(1) }, + InErr.B -> { err(OutErr.Abort) }, + }; + } + + fn handleRecoveringInIf(flag: bool, v: int) -> result int { + if flag { + handle source(v) { + InErr.A -> { ok(1) }, + InErr.B -> { err(OutErr.Abort) }, + }; + } else { + return err(OutErr.Abort); + } + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics); + + final var resultFallthroughCount = diagnostics.stream() + .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name())) + .count(); + assertTrue(resultFallthroughCount >= 2); + } }