implements PR-13.2

This commit is contained in:
bQUARKz 2026-03-10 10:27:51 +00:00
parent f8f520d284
commit 1137eaad56
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
3 changed files with 244 additions and 192 deletions

View File

@ -11,11 +11,14 @@ 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);
private final PbsFlowCompletionAnalyzer completionAnalyzer = new PbsFlowCompletionAnalyzer();
private final PbsFlowCallableBodyAnalyzer callableBodyAnalyzer = new PbsFlowCallableBodyAnalyzer(
typeOps,
completionAnalyzer,
this::analyzeBlock);
private record AssignmentTargetResolution(
TypeView type,
@ -115,196 +118,7 @@ final class PbsFlowBodyAnalyzer {
final ReadOnlyList<PbsAst.Parameter> parameters,
final PbsAst.Block body,
final PbsFlowBodyContext context) {
final var scope = context.scope();
for (final var parameter : parameters) {
scope.bind(
parameter.name(),
typeOps.typeFromTypeRef(parameter.typeRef(), context.model(), context.receiverType()));
}
final var callableContext = context.withScope(scope);
analyzeBlock(body, callableContext, true);
validateCallableCompletion(body, callableContext);
}
private void validateCallableCompletion(
final PbsAst.Block body,
final PbsFlowBodyContext context) {
final var returnType = context.returnType();
final var model = context.model();
final var diagnostics = context.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) {
Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name(),
"Possible fallthrough in result callable body; all paths must return explicitly",
body.span());
return;
}
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) {
return switch (expression) {
case PbsAst.GroupExpr groupExpr -> expressionAlwaysReturns(groupExpr.expression(), model);
case PbsAst.BlockExpr blockExpr -> blockAlwaysReturns(blockExpr.block(), model);
case PbsAst.IfExpr ifExpr -> blockAlwaysReturns(ifExpr.thenBlock(), model)
&& expressionAlwaysReturns(ifExpr.elseExpression(), model);
case PbsAst.SwitchExpr switchExpr -> switchAlwaysReturns(switchExpr, model);
case PbsAst.HandleExpr handleExpr -> handleAlwaysReturns(handleExpr, model);
case null, default -> 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) {
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);
callableBodyAnalyzer.validateCallableBody(parameters, body, context);
}
private TypeView analyzeBlock(

View File

@ -0,0 +1,40 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.utilities.structures.ReadOnlyList;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.TypeView;
final class PbsFlowCallableBodyAnalyzer {
@FunctionalInterface
interface BlockAnalysisDelegate {
TypeView analyze(PbsAst.Block block, PbsFlowBodyContext context, boolean valueContext);
}
private final PbsFlowTypeOps typeOps;
private final PbsFlowCompletionAnalyzer completionAnalyzer;
private final BlockAnalysisDelegate blockAnalysisDelegate;
PbsFlowCallableBodyAnalyzer(
final PbsFlowTypeOps typeOps,
final PbsFlowCompletionAnalyzer completionAnalyzer,
final BlockAnalysisDelegate blockAnalysisDelegate) {
this.typeOps = typeOps;
this.completionAnalyzer = completionAnalyzer;
this.blockAnalysisDelegate = blockAnalysisDelegate;
}
void validateCallableBody(
final ReadOnlyList<PbsAst.Parameter> parameters,
final PbsAst.Block body,
final PbsFlowBodyContext context) {
final var scope = context.scope();
for (final var parameter : parameters) {
scope.bind(
parameter.name(),
typeOps.typeFromTypeRef(parameter.typeRef(), context.model(), context.receiverType()));
}
final var callableContext = context.withScope(scope);
blockAnalysisDelegate.analyze(body, callableContext, true);
completionAnalyzer.validateCallableCompletion(body, callableContext);
}
}

View File

@ -0,0 +1,198 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.diagnostics.Diagnostics;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Kind;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Model;
import java.util.HashSet;
final class PbsFlowCompletionAnalyzer {
void validateCallableCompletion(
final PbsAst.Block body,
final PbsFlowBodyContext context) {
final var returnType = context.returnType();
final var model = context.model();
final var diagnostics = context.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) {
Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_POSSIBLE_FALLTHROUGH_RESULT.name(),
"Possible fallthrough in result callable body; all paths must return explicitly",
body.span());
return;
}
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) {
return switch (expression) {
case PbsAst.GroupExpr groupExpr -> expressionAlwaysReturns(groupExpr.expression(), model);
case PbsAst.BlockExpr blockExpr -> blockAlwaysReturns(blockExpr.block(), model);
case PbsAst.IfExpr ifExpr -> blockAlwaysReturns(ifExpr.thenBlock(), model)
&& expressionAlwaysReturns(ifExpr.elseExpression(), model);
case PbsAst.SwitchExpr switchExpr -> switchAlwaysReturns(switchExpr, model);
case PbsAst.HandleExpr handleExpr -> handleAlwaysReturns(handleExpr, model);
case null, default -> 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) {
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 PbsAst.Expression unwrapGroup(final PbsAst.Expression expression) {
if (expression instanceof PbsAst.GroupExpr groupExpr) {
return unwrapGroup(groupExpr.expression());
}
return expression;
}
}