[FIR] Create FlowPath interface and add alternate flows to CFGNodes

Preliminary refactor in preparation for the implementation details to
fix KT-56888. This change makes it so alternate data flows can be
created and maintained through CFGNodes. This change was separated and
created first to aid in the code review process.
This commit is contained in:
Brian Norman
2023-07-19 08:37:15 -05:00
committed by Space Team
parent 1acdb3c143
commit f3847de1b9
3 changed files with 170 additions and 43 deletions
@@ -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
}
@@ -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<out E : FirElement>(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<out E : FirElement>(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<FlowPath, PersistentFlow>? = null
open val alternateFlowPaths: Set<FlowPath>
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<FirStub>(owner, le
@CfgInternals
set(_) = throw IllegalStateException("can't set flow for stub node")
override val alternateFlowPaths: Set<FlowPath>
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 <R, D> accept(visitor: ControlFlowGraphVisitor<R, D>, data: D): R {
return visitor.visitStubNode(this, data)
}