diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt index 7d58bf415f1..9657fe6d4b9 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt @@ -154,9 +154,9 @@ abstract class FirDataFlowAnalyzer( graphBuilder.enterFunction(function) } localFunctionNode?.mergeIncomingFlow() - functionEnterNode.mergeIncomingFlow { + functionEnterNode.mergeIncomingFlow { _, flow -> if (function is FirAnonymousFunction && function.invocationKind?.canBeRevisited() == true) { - enterRepeatableStatement(it, function) + enterRepeatableStatement(flow, function) } } context.variableAssignmentAnalyzer.enterFunction(function) @@ -239,7 +239,7 @@ abstract class FirDataFlowAnalyzer( // ----------------------------------- Code Fragment ------------------------------------------ fun enterCodeFragment(codeFragment: FirCodeFragment) { - graphBuilder.enterCodeFragment(codeFragment).mergeIncomingFlow { flow -> + graphBuilder.enterCodeFragment(codeFragment).mergeIncomingFlow { _, flow -> val realVariablesFromContext = codeFragment.codeFragmentContext?.variables.orEmpty() for ((symbol, exactTypes) in realVariablesFromContext) { val realVariable = variableStorage.getOrCreateIfReal(flow, symbol.fir) as? RealVariable ?: continue @@ -316,9 +316,9 @@ abstract class FirDataFlowAnalyzer( // ----------------------------------- Operator call ----------------------------------- fun exitTypeOperatorCall(typeOperatorCall: FirTypeOperatorCall) { - graphBuilder.exitTypeOperatorCall(typeOperatorCall).mergeIncomingFlow { + graphBuilder.exitTypeOperatorCall(typeOperatorCall).mergeIncomingFlow { _, flow -> if (typeOperatorCall.operation !in FirOperation.TYPES) return@mergeIncomingFlow - addTypeOperatorStatements(it, typeOperatorCall) + addTypeOperatorStatements(flow, typeOperatorCall) } } @@ -404,7 +404,7 @@ abstract class FirDataFlowAnalyzer( val leftIsNull = leftIsNullConst || leftOperand.coneType.isNullableNothing && !rightIsNullConst val rightIsNull = rightIsNullConst || rightOperand.coneType.isNullableNothing && !leftIsNullConst - node.mergeIncomingFlow { flow -> + node.mergeIncomingFlow { _, flow -> when { leftConst != null && rightConst != null -> return@mergeIncomingFlow leftIsNull -> processEqNull(flow, equalityOperatorCall, rightOperand, operation.isEq()) @@ -559,7 +559,7 @@ abstract class FirDataFlowAnalyzer( } fun exitCheckNotNullCall(checkNotNullCall: FirCheckNotNullCall, callCompleted: Boolean) { - graphBuilder.exitCheckNotNullCall(checkNotNullCall, callCompleted).mergeIncomingFlow { flow -> + graphBuilder.exitCheckNotNullCall(checkNotNullCall, callCompleted).mergeIncomingFlow { _, flow -> val argumentVariable = variableStorage.getOrCreateIfReal(flow, checkNotNullCall.argument) ?: return@mergeIncomingFlow flow.commitOperationStatement(argumentVariable notEq null) } @@ -575,7 +575,7 @@ abstract class FirDataFlowAnalyzer( graphBuilder.enterWhenBranchCondition(whenBranch).mergeWhenBranchEntryFlow() } - private fun CFGNode<*>.mergeWhenBranchEntryFlow() = mergeIncomingFlow { flow -> + private fun CFGNode<*>.mergeWhenBranchEntryFlow() = mergeIncomingFlow { _, flow -> val previousConditionExitNode = previousNodes.singleOrNull() as? WhenBranchConditionExitNode ?: return@mergeIncomingFlow val previousCondition = previousConditionExitNode.fir.condition if (!previousCondition.coneType.isBoolean) return@mergeIncomingFlow @@ -586,7 +586,7 @@ abstract class FirDataFlowAnalyzer( fun exitWhenBranchCondition(whenBranch: FirWhenBranch) { val (conditionExitNode, resultEnterNode) = graphBuilder.exitWhenBranchCondition(whenBranch) conditionExitNode.mergeIncomingFlow() - resultEnterNode.mergeIncomingFlow { flow -> + resultEnterNode.mergeIncomingFlow { _, flow -> // If the condition is invalid, don't generate smart casts to Any or Boolean. if (whenBranch.condition.coneType.isBoolean) { val conditionVariable = variableStorage.get(flow, whenBranch.condition) ?: return@mergeIncomingFlow @@ -614,13 +614,13 @@ abstract class FirDataFlowAnalyzer( fun enterWhileLoop(loop: FirLoop) { val (loopEnterNode, loopConditionEnterNode) = graphBuilder.enterWhileLoop(loop) loopEnterNode.mergeIncomingFlow() - loopConditionEnterNode.mergeIncomingFlow { flow -> enterRepeatableStatement(flow, loop) } + loopConditionEnterNode.mergeIncomingFlow { _, flow -> enterRepeatableStatement(flow, loop) } } fun exitWhileLoopCondition(loop: FirLoop) { val (loopConditionExitNode, loopBlockEnterNode) = graphBuilder.exitWhileLoopCondition(loop) loopConditionExitNode.mergeIncomingFlow() - loopBlockEnterNode.mergeIncomingFlow { flow -> + loopBlockEnterNode.mergeIncomingFlow { _, flow -> if (loop.condition.coneType.isBoolean) { val conditionVariable = variableStorage.get(flow, loop.condition) ?: return@mergeIncomingFlow flow.commitOperationStatement(conditionVariable eq true) @@ -631,9 +631,9 @@ abstract class FirDataFlowAnalyzer( fun exitWhileLoop(loop: FirLoop) { val (conditionEnterNode, blockExitNode, exitNode) = graphBuilder.exitWhileLoop(loop) blockExitNode.mergeIncomingFlow() - exitNode.mergeIncomingFlow { - processWhileLoopExit(it, exitNode, conditionEnterNode) - processLoopExit(it, exitNode, exitNode.firstPreviousNode as LoopConditionExitNode) + exitNode.mergeIncomingFlow { _, flow -> + processWhileLoopExit(flow, exitNode, conditionEnterNode) + processLoopExit(flow, exitNode, exitNode.firstPreviousNode as LoopConditionExitNode) } } @@ -697,7 +697,7 @@ abstract class FirDataFlowAnalyzer( fun enterDoWhileLoop(loop: FirLoop) { val (loopEnterNode, loopBlockEnterNode) = graphBuilder.enterDoWhileLoop(loop) - loopEnterNode.mergeIncomingFlow { flow -> enterRepeatableStatement(flow, loop) } + loopEnterNode.mergeIncomingFlow { _, flow -> enterRepeatableStatement(flow, loop) } loopBlockEnterNode.mergeIncomingFlow() } @@ -710,7 +710,9 @@ abstract class FirDataFlowAnalyzer( fun exitDoWhileLoop(loop: FirLoop) { val (loopConditionExitNode, loopExitNode) = graphBuilder.exitDoWhileLoop(loop) loopConditionExitNode.mergeIncomingFlow() - loopExitNode.mergeIncomingFlow { processLoopExit(it, loopExitNode, loopConditionExitNode) } + loopExitNode.mergeIncomingFlow { _, flow -> + processLoopExit(flow, loopExitNode, loopConditionExitNode) + } exitRepeatableStatement(loop) } @@ -749,7 +751,7 @@ abstract class FirDataFlowAnalyzer( // ----------------------------------- Resolvable call ----------------------------------- fun exitQualifiedAccessExpression(qualifiedAccessExpression: FirQualifiedAccessExpression) { - graphBuilder.exitQualifiedAccessExpression(qualifiedAccessExpression).mergeIncomingFlow { flow -> + graphBuilder.exitQualifiedAccessExpression(qualifiedAccessExpression).mergeIncomingFlow { _, flow -> processConditionalContract(flow, qualifiedAccessExpression) } } @@ -759,7 +761,7 @@ abstract class FirDataFlowAnalyzer( } fun enterSafeCallAfterNullCheck(safeCall: FirSafeCallExpression) { - graphBuilder.enterSafeCall(safeCall).mergeIncomingFlow { flow -> + graphBuilder.enterSafeCall(safeCall).mergeIncomingFlow { _, flow -> val receiverVariable = variableStorage.getOrCreateIfReal(flow, safeCall.receiver) ?: return@mergeIncomingFlow flow.commitOperationStatement(receiverVariable notEq null) } @@ -767,7 +769,7 @@ abstract class FirDataFlowAnalyzer( fun exitSafeCall(safeCall: FirSafeCallExpression) { val node = graphBuilder.exitSafeCall() - node.mergeIncomingFlow { flow -> + node.mergeIncomingFlow { _, flow -> // If there is only 1 previous node, then this is LHS of `a?.b ?: c`; then the null-case // edge from `a` goes directly to `c` and this node's flow already assumes `b` executed. if (node.previousNodes.size < 2) return@mergeIncomingFlow @@ -798,8 +800,8 @@ abstract class FirDataFlowAnalyzer( fun exitFunctionCall(functionCall: FirFunctionCall, callCompleted: Boolean) { context.variableAssignmentAnalyzer.exitFunctionCall(callCompleted) - graphBuilder.exitFunctionCall(functionCall, callCompleted).mergeIncomingFlow { - processConditionalContract(it, functionCall) + graphBuilder.exitFunctionCall(functionCall, callCompleted).mergeIncomingFlow { _, flow -> + processConditionalContract(flow, functionCall) } } @@ -907,14 +909,14 @@ abstract class FirDataFlowAnalyzer( } fun exitLocalVariableDeclaration(variable: FirProperty, hadExplicitType: Boolean) { - graphBuilder.exitVariableDeclaration(variable).mergeIncomingFlow { flow -> + graphBuilder.exitVariableDeclaration(variable).mergeIncomingFlow { _, flow -> val initializer = variable.initializer ?: return@mergeIncomingFlow exitVariableInitialization(flow, initializer, variable, assignmentLhs = null, hadExplicitType) } } fun exitVariableAssignment(assignment: FirVariableAssignment) { - graphBuilder.exitVariableAssignment(assignment).mergeIncomingFlow { flow -> + graphBuilder.exitVariableAssignment(assignment).mergeIncomingFlow { _, flow -> val property = assignment.calleeReference?.toResolvedPropertySymbol()?.fir ?: return@mergeIncomingFlow if (property.isLocal || property.isVal) { exitVariableInitialization(flow, assignment.rValue, property, assignment.lValue, hasExplicitType = false) @@ -990,7 +992,7 @@ abstract class FirDataFlowAnalyzer( fun exitLeftBinaryLogicExpressionArgument(binaryLogicExpression: FirBinaryLogicExpression) { val (leftExitNode, rightEnterNode) = graphBuilder.exitLeftBinaryLogicExpressionArgument(binaryLogicExpression) leftExitNode.mergeIncomingFlow() - rightEnterNode.mergeIncomingFlow { flow -> + rightEnterNode.mergeIncomingFlow { _, flow -> val leftOperandVariable = variableStorage.get(flow, binaryLogicExpression.leftOperand) ?: return@mergeIncomingFlow val isAnd = binaryLogicExpression.kind == LogicOperationKind.AND flow.commitOperationStatement(leftOperandVariable eq isAnd) @@ -1001,7 +1003,7 @@ abstract class FirDataFlowAnalyzer( graphBuilder.exitBinaryLogicExpression(binaryLogicExpression).mergeBinaryLogicOperatorFlow() } - private fun AbstractBinaryExitNode.mergeBinaryLogicOperatorFlow() = mergeIncomingFlow { flow -> + private fun AbstractBinaryExitNode.mergeBinaryLogicOperatorFlow() = mergeIncomingFlow { _, flow -> val isAnd = fir.kind == LogicOperationKind.AND val flowFromLeft = leftOperandNode.flow val flowFromRight = rightOperandNode.flow @@ -1101,11 +1103,13 @@ abstract class FirDataFlowAnalyzer( fun exitElvisLhs(elvisExpression: FirElvisExpression) { val (lhsExitNode, lhsIsNotNullNode, rhsEnterNode) = graphBuilder.exitElvisLhs(elvisExpression) - val lhsVariable = variableStorage.getOrCreateIfReal(lhsExitNode.mergeIncomingFlow(), elvisExpression.lhs) - lhsIsNotNullNode.mergeIncomingFlow { flow -> + lhsExitNode.mergeIncomingFlow() + + val lhsVariable = variableStorage.getOrCreateIfReal(lhsExitNode.flow, elvisExpression.lhs) + lhsIsNotNullNode.mergeIncomingFlow { _, flow -> lhsVariable?.let { flow.commitOperationStatement(it notEq null) } } - rhsEnterNode.mergeIncomingFlow { flow -> + rhsEnterNode.mergeIncomingFlow { _, flow -> lhsVariable?.let { flow.commitOperationStatement(it eq null) } } } @@ -1113,7 +1117,7 @@ abstract class FirDataFlowAnalyzer( @OptIn(UnexpandedTypeCheck::class) fun exitElvis(elvisExpression: FirElvisExpression, isLhsNotNull: Boolean, callCompleted: Boolean) { val node = graphBuilder.exitElvis(isLhsNotNull, callCompleted) - node.mergeIncomingFlow { flow -> + node.mergeIncomingFlow { _, flow -> // If LHS is never null, then the edge from RHS is dead and this node's flow already contains // all statements from LHS unconditionally. if (isLhsNotNull) return@mergeIncomingFlow @@ -1169,13 +1173,19 @@ abstract class FirDataFlowAnalyzer( // Generally when calling some method on `graphBuilder`, one of the nodes it returns is the new `lastNode`. // In that case `mergeIncomingFlow` will automatically ensure consistency once called on that node. - private fun CFGNode<*>.buildIncomingFlow(): MutableFlow { + private fun CFGNode<*>.buildIncomingFlow( + path: FlowPath, + builder: (FlowPath, MutableFlow) -> Unit, + ): MutableFlow { val previousFlows = previousNodes.mapNotNull { val incomingEdgeKind = edgeFrom(it).kind if (if (isDead) !incomingEdgeKind.usedInDeadDfa else !incomingEdgeKind.usedInDfa) return@mapNotNull null + // `MergePostponedLambdaExitsNode` nodes form a parallel data flow graph. We never compute // data flow for any of them until reaching a completed call. - if (it is MergePostponedLambdaExitsNode) it.mergeIncomingFlow() else it.flow + if (it is MergePostponedLambdaExitsNode) it.mergeIncomingFlow() + + it.flow } val result = logicSystem.joinFlow(previousFlows, isUnion) if (graphBuilder.lastNodeOrNull == this) { @@ -1186,24 +1196,20 @@ abstract class FirDataFlowAnalyzer( } currentReceiverState = result } - return result + return result.also { builder(path, it) } } @OptIn(CfgInternals::class) - private fun CFGNode<*>.setFlow(builder: MutableFlow): PersistentFlow { - val flow = builder.freeze().also { this.flow = it } - if (currentReceiverState === builder) { - currentReceiverState = flow + private fun CFGNode<*>.mergeIncomingFlow( + builder: (FlowPath, MutableFlow) -> Unit = { _, _ -> }, + ) { + val mutableDefaultFlow = buildIncomingFlow(FlowPath.Default, builder) + val defaultFlow = mutableDefaultFlow.freeze().also { this.flow = it } + if (currentReceiverState === mutableDefaultFlow) { + currentReceiverState = defaultFlow } - return flow } - private inline fun CFGNode<*>.mergeIncomingFlow(crossinline builder: (MutableFlow) -> Unit): PersistentFlow = - setFlow(buildIncomingFlow().also(builder)) - - private fun CFGNode<*>.mergeIncomingFlow() = - setFlow(buildIncomingFlow()) - // In rare cases (like after exiting functions) after adding more nodes `graphBuilder` will revert the current // state to a previously created node, so none of the nodes it returned are `lastNode` and `mergeIncomingFlow` // will not ensure consistency. In that case an explicit call to `resetReceivers` is needed to roll back the stack @@ -1251,4 +1257,10 @@ abstract class FirDataFlowAnalyzer( private fun MutableFlow.commitOperationStatement(statement: OperationStatement) = addAllStatements(logicSystem.approveOperationStatement(this, statement, removeApprovedOrImpossible = true)) + private enum class FlowPathOperation { + PROPAGATE, + ADDITIONAL, + DIVERGE, + TERMINATE, + } } diff --git a/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/FlowPath.kt b/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/FlowPath.kt new file mode 100644 index 00000000000..4b9d1be91c2 --- /dev/null +++ b/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/FlowPath.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.fir.resolve.dfa + +import org.jetbrains.kotlin.fir.FirElement +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.EdgeLabel + +/** + * Sealed interface representing a type of path through a [Control Flow Graph (CFG) Node][org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode] + * used for data flow analysis. Most CFG nodes only have a single data flow path through them, but there are times when a node may require + * multiple paths to be calculated. The most common use case is for `finally` code blocks, where multiple code paths may enter, but these + * paths diverge after exiting the code block. Consider the following (very) contrived example: + * + * ```kotlin + * fun test() { + * var x: Any? = null + * while (true) { + * try { + * x = "" // (1) + * doSomething() + * } catch (e: Exception) { + * x = 1 // (2) + * break // (3) + * } finally { + * x.inc() // (4) Should be error. + * x.length // (5) Should be error. + * } + * x.length // (6) Should be OK. + * } + * x.inc() // (7) Should be OK. + * } + * ``` + * + * A few things to note here: + * + * 1. The statement at (1) implies all following code can smartcast `x` to be a `String`. + * 2. The statement at (2) implies all following code can smartcast `x` to be a `Int`. + * 3. The statement at (3) is the only way to exit the infinite `while` loop without throwing an exception. + * 4. Both of the statements at (4) and (5) should be considered errors because it is unknown through which path they will be reached. It is + * possible to reach them only through (1) or after an exception is caught and going through (2). + * 5. The statement at (6) should compile successfully since the only way to reach it is if the `try-catch-finally` expression completes + * without exception, therefore only executing the statement at (1) and ***not*** (2). + * 6. The statement at (7) should compile successfully since the only way to reach it is if an exception is caught and processed by the + * `catch` block, therefore executing the statement at (2). + * + * To achieve this behavior, multiple data flows must be maintained through the `finally` code block for use by each path that comes after. + * In the above example, 3 separate flows must be maintained: + * + * 1. A flow which will be used within the `finally` block itself. This data flow is a combination of all flows which lead into the + * `finally` block. + * 2. A flow which will be used when the entire `try` expression exits by jumping out of the while loop. This data flow is a continuation of + * the data flow leading into the `finally` block from the `break` statement. + * 3. A flow which will be used when the entire `try` expression exits without exception or jumping. This data flow is a continuation of the + * data flow leading into the `finally` block from the main `try` block. + */ +sealed interface FlowPath { + /** + * The [FlowPath] which represents the combination of all flows leading into a CFG Node. + */ + data object Default : FlowPath + + /** + * The [FlowPath] which represents the combination of all flows leading into a CFG Node that follow an edge with the specified + * [edge label][EdgeLabel]. The edge label is also combined with an [FIR element][FirElement] as the same edge label can be used for + * multiple flows. For example, a `finally` block within another `finally` block will require multiple + * [normal paths][org.jetbrains.kotlin.fir.resolve.dfa.cfg.NormalPath] through each that diverge at different nodes. + */ + data class CfgEdge(val label: EdgeLabel, val fir: FirElement) : FlowPath +} diff --git a/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/CFGNode.kt b/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/CFGNode.kt index 10ac5189b4f..6f2fe99b0fc 100644 --- a/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/CFGNode.kt +++ b/compiler/fir/semantics/src/org/jetbrains/kotlin/fir/resolve/dfa/cfg/CFGNode.kt @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.fir.FirElement import org.jetbrains.kotlin.fir.declarations.* import org.jetbrains.kotlin.fir.expressions.* +import org.jetbrains.kotlin.fir.resolve.dfa.FlowPath import org.jetbrains.kotlin.fir.resolve.dfa.PersistentFlow import org.jetbrains.kotlin.fir.resolve.dfa.controlFlowGraph import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol @@ -113,6 +114,10 @@ sealed class CFGNode(val owner: ControlFlowGraph, val level: var isDead: Boolean = false protected set + /** + * [Flow][org.jetbrains.kotlin.fir.resolve.dfa.Flow] representing the [default path][FlowPath.Default] for this node. This flow should + * be used for all type resolutions at this node. + */ private var _flow: PersistentFlow? = null open var flow: PersistentFlow get() = _flow ?: throw IllegalStateException("flow for $this not initialized - traversing nodes in wrong order?") @@ -122,6 +127,32 @@ sealed class CFGNode(val owner: ControlFlowGraph, val level: _flow = value } + /** + * All other [flows][org.jetbrains.kotlin.fir.resolve.dfa.Flow] through this node which are not the [default][FlowPath.Default]. These + * flows should only be used by following nodes when the path through this node diverges (ex., following a `finally` code block). + */ + private var _alternateFlows: MutableMap? = null + open val alternateFlowPaths: Set + get() = _alternateFlows?.keys ?: emptySet() + + open fun getAlternateFlow(path: FlowPath): PersistentFlow? { + return _alternateFlows?.get(path) + } + + @CfgInternals + open fun addAlternateFlow(path: FlowPath, flow: PersistentFlow) { + assert(path !== FlowPath.Default) { "cannot add default flow path as alternate for $this" } + assert(_alternateFlows?.get(path) == null) { "reassigning $path flow for $this" } + + var alternateFlows = _alternateFlows + if (alternateFlows == null) { + alternateFlows = mutableMapOf() + _alternateFlows = alternateFlows + } + + alternateFlows[path] = flow + } + @CfgInternals fun updateDeadStatus() { isDead = if (isUnion) @@ -746,6 +777,18 @@ class StubNode(owner: ControlFlowGraph, level: Int) : CFGNode(owner, le @CfgInternals set(_) = throw IllegalStateException("can't set flow for stub node") + override val alternateFlowPaths: Set + get() = firstPreviousNode.alternateFlowPaths + + override fun getAlternateFlow(path: FlowPath): PersistentFlow? { + return firstPreviousNode.getAlternateFlow(path) + } + + @CfgInternals + override fun addAlternateFlow(path: FlowPath, flow: PersistentFlow) { + firstPreviousNode.addAlternateFlow(path, flow) + } + override fun accept(visitor: ControlFlowGraphVisitor, data: D): R { return visitor.visitStubNode(this, data) }