implements PR021
This commit is contained in:
parent
d2287a0a58
commit
d7b070ab68
@ -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`
|
20. `PR-018-pbs-result-flow-ok-err-handle-rules.md`
|
||||||
21. `PR-019-pbs-non-unit-fallthrough-diagnostics.md`
|
21. `PR-019-pbs-non-unit-fallthrough-diagnostics.md`
|
||||||
22. `PR-020-pbs-lowering-admission-gates.md`
|
22. `PR-020-pbs-lowering-admission-gates.md`
|
||||||
|
23. `PR-021-pbs-handle-aware-fallthrough-completion.md`
|
||||||
|
|||||||
@ -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.
|
|
||||||
@ -180,9 +180,57 @@ final class PbsFlowBodyAnalyzer {
|
|||||||
if (expression instanceof PbsAst.SwitchExpr switchExpr) {
|
if (expression instanceof PbsAst.SwitchExpr switchExpr) {
|
||||||
return switchAlwaysReturns(switchExpr, model);
|
return switchAlwaysReturns(switchExpr, model);
|
||||||
}
|
}
|
||||||
|
if (expression instanceof PbsAst.HandleExpr handleExpr) {
|
||||||
|
return handleAlwaysReturns(handleExpr, model);
|
||||||
|
}
|
||||||
return false;
|
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(
|
private boolean switchAlwaysReturns(
|
||||||
final PbsAst.SwitchExpr switchExpr,
|
final PbsAst.SwitchExpr switchExpr,
|
||||||
final Model model) {
|
final Model model) {
|
||||||
|
|||||||
@ -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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
class PbsSemanticsFallthroughTest {
|
class PbsSemanticsFallthroughTest {
|
||||||
|
|
||||||
@ -110,4 +111,100 @@ class PbsSemanticsFallthroughTest {
|
|||||||
assertFalse(diagnostics.stream().anyMatch(d ->
|
assertFalse(diagnostics.stream().anyMatch(d ->
|
||||||
d.getCode().equals(PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name())));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user