implements PR019
This commit is contained in:
parent
9bf9b20a4f
commit
eb469fd68c
@ -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<E>` 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.
|
||||
@ -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<E>` 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<E>` 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.
|
||||
@ -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<String>();
|
||||
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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<Err> 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<Err> 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())));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user