[FIR] Create alternate DFA flows through finally blocks

Entering a `finally` block can happen from many different places:
through an exception, a jump, or normal exit from the `try` block. When
in the `finally` block, all DFA flows must be merged to have correct
smart casting. However, after the `finally` block, if exiting normally
or because of a jump, the combined flow from within the `finally` block
should not be used, but rather an alternate flow which combines the
correct flows from before the `finally` block.

```
try {
    str as String // Potential cast exception
} finally {
    str.length // Shouldn`t be resolved
}
str.length // Should be resolved
```

When building DFA flows, track the start of possible alternate flows,
and continue building them until they end. Both of these situations are
now marked on CFGNodes via interfaces.

When building the default DFA flow, and the source node is the end node
of alternate flows, attempt to use the alternate flow with the same edge
label instead of the default flow of the source node.

#KT-56888 Fixed
This commit is contained in:
Brian Norman
2023-07-20 07:05:53 -05:00
committed by Space Team
parent f3847de1b9
commit 0e2b3ce845
18 changed files with 677 additions and 546 deletions
@@ -9,8 +9,8 @@ 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
* Sealed class 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:
*
@@ -56,11 +56,11 @@ import org.jetbrains.kotlin.fir.resolve.dfa.cfg.EdgeLabel
* 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 {
sealed class FlowPath {
/**
* The [FlowPath] which represents the combination of all flows leading into a CFG Node.
*/
data object Default : FlowPath
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
@@ -68,5 +68,5 @@ sealed interface FlowPath {
* 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
data class CfgEdge(val label: EdgeLabel, val fir: FirElement) : FlowPath()
}
@@ -170,11 +170,22 @@ sealed class CFGNode<out E : FirElement>(val owner: ControlFlowGraph, val level:
val CFGNode<*>.firstPreviousNode: CFGNode<*> get() = previousNodes[0]
val CFGNode<*>.lastPreviousNode: CFGNode<*> get() = previousNodes.last()
val CFGNode<*>.previousDfaNodes: Sequence<Pair<Edge, CFGNode<*>>>
get() = previousNodes.asSequence()
.map { edgeFrom(it) to it }
.filter { (edge, _) -> if (isDead) edge.kind.usedInDeadDfa else edge.kind.usedInDfa }
val CFGNode<*>.previousLiveNodes: Sequence<CFGNode<*>>
get() = when {
this.isDead -> previousNodes.asSequence()
else -> previousNodes.asSequence().mapNotNull { it.takeIf { !it.isDead } }
}
interface EnterNodeMarker
interface ExitNodeMarker
interface GraphEnterNodeMarker : EnterNodeMarker
interface GraphExitNodeMarker : ExitNodeMarker
interface AlternateFlowStartMarker
interface AlternateFlowEndMarker
// ----------------------------------- EnterNode for declaration with CFG -----------------------------------
@@ -260,6 +271,17 @@ class PostponedLambdaExitNode(owner: ControlFlowGraph, override val fir: FirAnon
}
class MergePostponedLambdaExitsNode(owner: ControlFlowGraph, override val fir: FirElement, level: Int) : CFGNode<FirElement>(owner, level) {
private var _flowInitialized = false
val flowInitialized: Boolean get() = _flowInitialized
override var flow: PersistentFlow
get() = super.flow
@CfgInternals
set(value) {
super.flow = value
_flowInitialized = true
}
override fun <R, D> accept(visitor: ControlFlowGraphVisitor<R, D>, data: D): R {
return visitor.visitMergePostponedLambdaExitsNode(this, data)
}
@@ -543,13 +565,13 @@ class CatchClauseExitNode(owner: ControlFlowGraph, override val fir: FirCatch, l
}
}
class FinallyBlockEnterNode(owner: ControlFlowGraph, override val fir: FirTryExpression, level: Int) : CFGNode<FirTryExpression>(owner, level),
EnterNodeMarker {
EnterNodeMarker, AlternateFlowStartMarker {
override fun <R, D> accept(visitor: ControlFlowGraphVisitor<R, D>, data: D): R {
return visitor.visitFinallyBlockEnterNode(this, data)
}
}
class FinallyBlockExitNode(owner: ControlFlowGraph, override val fir: FirTryExpression, level: Int) : CFGNode<FirTryExpression>(owner, level),
ExitNodeMarker {
ExitNodeMarker, AlternateFlowEndMarker {
override fun <R, D> accept(visitor: ControlFlowGraphVisitor<R, D>, data: D): R {
return visitor.visitFinallyBlockExitNode(this, data)
}