implements PR021

This commit is contained in:
bQUARKz 2026-03-06 09:35:56 +00:00
parent d2287a0a58
commit d7b070ab68
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 146 additions and 43 deletions

View File

@ -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`

View File

@ -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.

View File

@ -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) {

View File

@ -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<InErr> int {
if v > 0 { return err(InErr.A); }
return err(InErr.B);
}
fn handleShortRemapTerminal(v: int) -> result<OutErr> int {
handle source(v) {
InErr.A -> OutErr.Retry,
InErr.B -> OutErr.Abort,
};
}
fn handleBlockErrTerminal(v: int) -> result<OutErr> int {
handle source(v) {
InErr.A -> { err(OutErr.Retry) },
InErr.B -> { err(OutErr.Abort) },
};
}
fn handleTerminalInIf(flag: bool, v: int) -> result<OutErr> 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<OutErr> 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<InErr> int {
if v > 0 { return err(InErr.A); }
return err(InErr.B);
}
fn handleRecoveringTail(v: int) -> result<OutErr> int {
handle source(v) {
InErr.A -> { ok(1) },
InErr.B -> { err(OutErr.Abort) },
};
}
fn handleRecoveringInIf(flag: bool, v: int) -> result<OutErr> 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);
}
}