implements PR018

This commit is contained in:
bQUARKz 2026-03-05 20:07:50 +00:00
parent 2041e34496
commit 9bf9b20a4f
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 376 additions and 7 deletions

View File

@ -146,18 +146,14 @@ final class PbsFlowBodyAnalyzer {
}
if (statement instanceof PbsAst.ReturnStatement returnStatement) {
if (returnStatement.value() != null) {
expressionAnalyzer.analyzeExpression(
analyzeReturnStatement(
returnStatement.value(),
scope,
returnType,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
ExprUse.VALUE,
true,
this::analyzeBlock);
diagnostics);
}
return;
}
@ -292,6 +288,128 @@ final class PbsFlowBodyAnalyzer {
}
}
private void analyzeReturnStatement(
final PbsAst.Expression value,
final Scope scope,
final TypeView returnType,
final String resultErrorName,
final TypeView receiverType,
final Model model,
final DiagnosticSink diagnostics) {
final var root = unwrapGroup(value);
if (root instanceof PbsAst.OkExpr okExpr) {
analyzeReturnOk(okExpr, scope, returnType, resultErrorName, receiverType, model, diagnostics);
return;
}
if (root instanceof PbsAst.ErrExpr errExpr) {
analyzeReturnErr(errExpr, returnType, resultErrorName, model, diagnostics);
return;
}
expressionAnalyzer.analyzeExpression(
value,
scope,
returnType,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
ExprUse.VALUE,
true,
this::analyzeBlock);
}
private void analyzeReturnOk(
final PbsAst.OkExpr okExpr,
final Scope scope,
final TypeView returnType,
final String resultErrorName,
final TypeView receiverType,
final Model model,
final DiagnosticSink diagnostics) {
if (returnType.kind() != PbsFlowSemanticSupport.Kind.RESULT || resultErrorName == null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name(),
"'ok(...)' is only allowed when returning from a result callable",
okExpr.span());
expressionAnalyzer.analyzeExpression(
okExpr.value(),
scope,
null,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
ExprUse.VALUE,
true,
this::analyzeBlock);
return;
}
final var payloadType = returnType.inner();
final var actualType = expressionAnalyzer.analyzeExpression(
okExpr.value(),
scope,
payloadType,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
ExprUse.VALUE,
true,
this::analyzeBlock).type();
if (!typeOps.compatible(actualType, payloadType)) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name(),
"Payload in 'ok(...)' is incompatible with result payload type",
okExpr.value().span());
}
}
private void analyzeReturnErr(
final PbsAst.ErrExpr errExpr,
final TypeView returnType,
final String resultErrorName,
final Model model,
final DiagnosticSink diagnostics) {
if (returnType.kind() != PbsFlowSemanticSupport.Kind.RESULT || resultErrorName == null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name(),
"'err(...)' is only allowed when returning from a result callable",
errExpr.span());
return;
}
if (!matchesTargetError(errExpr.errorPath(), resultErrorName, model)) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name(),
"Error label in 'err(...)' does not match enclosing result error type",
errExpr.errorPath().span());
}
}
private PbsAst.Expression unwrapGroup(final PbsAst.Expression expression) {
if (expression instanceof PbsAst.GroupExpr groupExpr) {
return unwrapGroup(groupExpr.expression());
}
return expression;
}
private boolean matchesTargetError(
final PbsAst.ErrorPath path,
final String resultErrorName,
final Model model) {
if (path == null || resultErrorName == null || path.segments().size() != 2) {
return false;
}
final var errorName = path.segments().getFirst();
final var caseName = path.segments().get(1);
final var targetCases = model.errors.get(errorName);
return resultErrorName.equals(errorName) && targetCases != null && targetCases.contains(caseName);
}
private void analyzeAssignmentStatement(
final PbsAst.AssignStatement assignStatement,
final Scope scope,

View File

@ -509,6 +509,10 @@ final class PbsFlowExpressionAnalyzer {
}
if (expression instanceof PbsAst.OkExpr okExpr) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name(),
"'ok(...)' is only allowed in result return flow and handle arm terminals",
okExpr.span());
analyzeExpressionInternal(
okExpr.value(),
scope,
@ -523,6 +527,10 @@ final class PbsFlowExpressionAnalyzer {
return ExprResult.type(TypeView.unknown());
}
if (expression instanceof PbsAst.ErrExpr errExpr) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name(),
"'err(...)' is only allowed in result return flow and handle arm terminals",
errExpr.span());
return ExprResult.type(TypeView.unknown());
}
@ -1062,6 +1070,7 @@ final class PbsFlowExpressionAnalyzer {
final var sourceErrorName = sourceType.errorType() == null ? null : sourceType.errorType().name();
final var sourceCases = sourceErrorName == null ? Set.<String>of() : model.errors.getOrDefault(sourceErrorName, Set.of());
final var sourcePayloadType = sourceType.inner() == null ? TypeView.unknown() : sourceType.inner();
final var matchedCases = new HashSet<String>();
var hasWildcard = false;
@ -1096,7 +1105,15 @@ final class PbsFlowExpressionAnalyzer {
}
continue;
}
blockAnalyzer.analyze(arm.block(), scope, returnType, resultErrorName, receiverType, model, diagnostics, true);
analyzeHandleBlockArm(
arm,
scope,
sourcePayloadType,
returnType,
resultErrorName,
receiverType,
model,
diagnostics);
}
if (!hasWildcard && !sourceCases.isEmpty() && !matchedCases.containsAll(sourceCases)) {
@ -1109,6 +1126,93 @@ final class PbsFlowExpressionAnalyzer {
return sourceType.inner() == null ? TypeView.unknown() : sourceType.inner();
}
private void analyzeHandleBlockArm(
final PbsAst.HandleArm arm,
final Scope scope,
final TypeView sourcePayloadType,
final TypeView returnType,
final String resultErrorName,
final TypeView receiverType,
final Model model,
final DiagnosticSink diagnostics) {
if (arm.block() == null) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_ARM_TERMINAL_INVALID.name(),
"Handle block arm must terminate with 'ok(...)' or 'err(E.case)'",
arm.span());
return;
}
final var block = arm.block();
final var terminal = unwrapGroup(block.tailExpression());
if (terminal instanceof PbsAst.OkExpr okExpr) {
final var payloadBlock = new PbsAst.Block(block.statements(), okExpr.value(), block.span());
final var actualPayloadType = blockAnalyzer.analyze(
payloadBlock,
scope,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
true);
if (!typeOps.compatible(actualPayloadType, sourcePayloadType)) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name(),
"Handle arm 'ok(...)' payload is incompatible with source result payload type",
okExpr.value().span());
}
return;
}
if (terminal instanceof PbsAst.ErrExpr errExpr) {
final var statementsOnly = new PbsAst.Block(block.statements(), null, block.span());
blockAnalyzer.analyze(
statementsOnly,
scope,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
false);
if (resultErrorName != null && !matchesTargetError(errExpr.errorPath(), resultErrorName, model)) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name(),
"Error label in handle arm 'err(...)' does not match enclosing result error type",
errExpr.errorPath().span());
}
return;
}
if (block.tailExpression() == null) {
final var statementsOnly = new PbsAst.Block(block.statements(), null, block.span());
blockAnalyzer.analyze(
statementsOnly,
scope,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
false);
} else {
blockAnalyzer.analyze(
block,
scope,
returnType,
resultErrorName,
receiverType,
model,
diagnostics,
true);
}
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_HANDLE_ARM_TERMINAL_INVALID.name(),
"Handle block arm must terminate with 'ok(...)' or 'err(E.case)'",
block.span());
}
private boolean matchesTargetError(
final PbsAst.ErrorPath path,
final String resultErrorName,
@ -1122,4 +1226,14 @@ final class PbsFlowExpressionAnalyzer {
return resultErrorName.equals(errorName) && targetCases != null && targetCases.contains(caseName);
}
private PbsAst.Expression unwrapGroup(final PbsAst.Expression expression) {
if (expression == null) {
return null;
}
if (expression instanceof PbsAst.GroupExpr groupExpr) {
return unwrapGroup(groupExpr.expression());
}
return expression;
}
}

View File

@ -41,8 +41,12 @@ public enum PbsSemanticsErrors {
E_SEM_NONE_WITHOUT_EXPECTED_OPTIONAL,
E_SEM_ELSE_NON_OPTIONAL_LEFT,
E_SEM_ELSE_FALLBACK_TYPE_MISMATCH,
E_SEM_RESULT_FLOW_INVALID_POSITION,
E_SEM_RESULT_OK_PAYLOAD_MISMATCH,
E_SEM_RESULT_ERROR_LABEL_INVALID,
E_SEM_RESULT_PROPAGATE_NON_RESULT,
E_SEM_RESULT_PROPAGATE_ERROR_MISMATCH,
E_SEM_HANDLE_NON_RESULT,
E_SEM_HANDLE_ERROR_MISMATCH,
E_SEM_HANDLE_ARM_TERMINAL_INVALID,
}

View File

@ -0,0 +1,133 @@
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PbsSemanticsResultFlowRulesTest {
@Test
void shouldAcceptValidResultReturnAndHandleArmTerminals() {
final var source = """
declare error InErr { A; B; }
declare error OutErr { Retry; Abort; }
fn inner(v: int) -> result<InErr> int {
if v > 0 { return ok(v); }
return err(InErr.A);
}
fn wrap(v: int) -> result<OutErr> int {
let recovered: int = handle inner(v) {
InErr.A -> { ok(1) },
InErr.B -> { err(OutErr.Retry) }
};
return ok(recovered);
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name())));
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name())));
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name())));
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_HANDLE_ARM_TERMINAL_INVALID.name())));
}
@Test
void shouldRejectOkAndErrOutsideAllowedResultFlowPositions() {
final var source = """
declare error Err { Fail; }
fn bad(v: int) -> int {
ok(v);
err(Err.Fail);
return v;
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name())));
}
@Test
void shouldRejectMismatchedResultPayloadAndErrorLabels() {
final var source = """
declare error ErrA { Fail; }
declare error ErrB { Stop; }
fn badPayload() -> result<ErrA> int {
return ok("oops");
}
fn badLabel() -> result<ErrA> int {
return err(ErrB.Stop);
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name())));
}
@Test
void shouldRejectInvalidHandleBlockTerminalAndInvalidHandleTerminalForms() {
final var source = """
declare error InErr { Fail; }
declare error OutErr { Retry; Abort; }
declare error OtherErr { Oops; }
fn source() -> result<InErr> int { return err(InErr.Fail); }
fn invalidTerminal() -> result<OutErr> int {
let value: int = handle source() {
InErr.Fail -> { 1 }
};
return ok(value);
}
fn badHandleOkPayload() -> result<OutErr> int {
let value: int = handle source() {
InErr.Fail -> {
let payload: str = "x";
ok(payload)
}
};
return ok(value);
}
fn badHandleErrLabel() -> result<OutErr> int {
let value: int = handle source() {
InErr.Fail -> { err(OtherErr.Oops) }
};
return ok(value);
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_HANDLE_ARM_TERMINAL_INVALID.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name())));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name())));
}
}