implements PR019

This commit is contained in:
bQUARKz 2026-03-05 20:13:49 +00:00
parent 9bf9b20a4f
commit eb469fd68c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 262 additions and 86 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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())));
}
}