FIR CFG: join/unify data flow from postponed lambdas at each level

For example:

    foo(
        // `if` joins A & B
        if (condition)
            run { ... } // A
        else
            run { ... }, // B
        run { ... } // C
    ) // `foo` unifies `A & B` and `C`, so if it is not resolved itself,
      // further `if`s, `when`s, safe calls outside it, etc. continue
      // building the correct type predicate until the next completed
      // call.

^KT-44512 Fixed
This commit is contained in:
pyos
2022-06-13 14:17:34 +02:00
committed by teamcity
parent 755c54553a
commit 63b0708ed5
17 changed files with 720 additions and 623 deletions
@@ -826,7 +826,7 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
}
fun exitWhenExpression(whenExpression: FirWhenExpression) {
val (whenExitNode, syntheticElseNode) = graphBuilder.exitWhenExpression(whenExpression)
val (whenExitNode, syntheticElseNode, mergePostponedLambdaExitsNode) = graphBuilder.exitWhenExpression(whenExpression)
if (syntheticElseNode != null) {
val previousConditionExitNode = syntheticElseNode.firstPreviousNode as? WhenBranchConditionExitNode
// previous node for syntheticElseNode can be not WhenBranchConditionExitNode in case of `when` without any branches
@@ -844,6 +844,7 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
}
}
whenExitNode.mergeIncomingFlow()
mergePostponedLambdaExitsNode?.mergeIncomingFlow()
}
fun exitWhenSubjectExpression(expression: FirWhenSubjectExpression) {
@@ -1022,8 +1023,9 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
}
fun exitSafeCall(safeCall: FirSafeCallExpression) {
val node = graphBuilder.exitSafeCall().mergeIncomingFlow()
val flow = node.flow
val (node, mergePostponedLambdaExitsNode) = graphBuilder.exitSafeCall()
val flow = node.mergeIncomingFlow().flow
mergePostponedLambdaExitsNode?.mergeIncomingFlow()
val variable = variableStorage.getOrCreateVariable(flow, safeCall)
val receiverVariable = when (variable) {
@@ -1462,7 +1464,9 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
}
fun exitElvis(elvisExpression: FirElvisExpression, isLhsNotNull: Boolean) {
val node = graphBuilder.exitElvis().mergeIncomingFlow()
val (node, mergePostponedLambdaExitsNode) = graphBuilder.exitElvis()
node.mergeIncomingFlow()
mergePostponedLambdaExitsNode?.mergeIncomingFlow()
if (isLhsNotNull) {
elvisExpression.lhs.propagateNotNullInfo(node)
}
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.util.ListMultimap
import org.jetbrains.kotlin.fir.util.listMultimapOf
import org.jetbrains.kotlin.fir.visitors.FirDefaultVisitor
import org.jetbrains.kotlin.utils.addToStdlib.popLast
import org.jetbrains.kotlin.utils.addToStdlib.runIf
import kotlin.random.Random
@@ -85,7 +86,7 @@ class ControlFlowGraphBuilder {
private val loopEnterNodes: NodeStorage<FirElement, CFGNode<FirElement>> = NodeStorage()
private val loopExitNodes: NodeStorage<FirLoop, LoopExitNode> = NodeStorage()
private val exitsFromCompletedPostponedAnonymousFunctions: MutableList<PostponedLambdaExitNode> = mutableListOf()
private val exitsFromCompletedPostponedAnonymousFunctions: MutableList<MutableList<CFGNode<*>>> = mutableListOf()
private val whenExitNodes: NodeStorage<FirWhenExpression, WhenExitNode> = NodeStorage()
private val whenBranchIndices: Stack<Map<FirWhenBranch, Int>> = stackOf()
@@ -341,7 +342,8 @@ class ControlFlowGraphBuilder {
val graph = popGraph().also { graph ->
assert(graph.declaration == anonymousFunction)
assert(graph.exitNode == exitNode)
exitsFromCompletedPostponedAnonymousFunctions.removeAll { it.owner == graph }
// TODO: disregarding the edges is probably not correct, though this should never find any nodes anyway
exitsFromCompletedPostponedAnonymousFunctions.lastOrNull()?.removeAll { it.owner == graph }
}
val postponedEnterNode = entersToPostponedAnonymousFunctions.remove(symbol)!!
@@ -361,7 +363,7 @@ class ControlFlowGraphBuilder {
}
if (invocationKind == EventOccurrencesRange.EXACTLY_ONCE && shouldPassFlowFromInplaceLambda.top()) {
exitsFromCompletedPostponedAnonymousFunctions += postponedExitNode
exitsFromCompletedPostponedAnonymousFunctions.lastOrNull()?.add(postponedExitNode)
}
val containingGraph = parentGraphForAnonymousFunctions.remove(symbol) ?: currentGraph
@@ -666,6 +668,7 @@ class ControlFlowGraphBuilder {
whenBranchIndices.push(whenExpression.branches.mapIndexed { index, branch -> branch to index }.toMap())
notCompletedFunctionCalls.push(mutableListOf())
levelCounter++
splitDataFlowForPostponedLambdas()
return node
}
@@ -696,7 +699,9 @@ class ControlFlowGraphBuilder {
return node
}
fun exitWhenExpression(whenExpression: FirWhenExpression): Pair<WhenExitNode, WhenSyntheticElseBranchNode?> {
fun exitWhenExpression(
whenExpression: FirWhenExpression
): Triple<WhenExitNode, WhenSyntheticElseBranchNode?, MergePostponedLambdaExitsNode?> {
val whenExitNode = whenExitNodes.pop()
// exit from last condition node still on stack
// we should remove it
@@ -710,10 +715,9 @@ class ControlFlowGraphBuilder {
} else null
whenExitNode.updateDeadStatus()
lastNodes.push(whenExitNode)
dropPostponedLambdasForNonDeterministicCalls()
levelCounter--
whenBranchIndices.pop()
return whenExitNode to syntheticElseBranchNode
return Triple(whenExitNode, syntheticElseBranchNode, joinDataFlowFromPostponedLambdasWith(whenExitNode))
}
// ----------------------------------- While Loop -----------------------------------
@@ -920,6 +924,7 @@ class ControlFlowGraphBuilder {
finallyExitNodes.push(createFinallyBlockExitNode(tryExpression))
}
notCompletedFunctionCalls.push(mutableListOf())
splitDataFlowForPostponedLambdas()
return enterTryExpressionNode to enterTryNodeBlock
}
@@ -1074,6 +1079,7 @@ class ControlFlowGraphBuilder {
fun enterCall() {
levelCounter++
splitDataFlowForPostponedLambdas()
}
fun exitIgnoredCall(functionCall: FirFunctionCall) {
@@ -1151,53 +1157,92 @@ class ControlFlowGraphBuilder {
return node to unionNode
}
/*
* This is needed for some control flow constructions which are resolved as calls (when and elvis)
* For usual call we have invariant that all arguments will be called before function call, but for
* when and elvis only one of arguments will be actually called, so it's illegal to pass data flow info
* from lambda in one of branches
*/
private fun dropPostponedLambdasForNonDeterministicCalls() {
exitsFromCompletedPostponedAnonymousFunctions.clear()
// Arguments are evaluated left to right, and this is how data flows.
// foo(run { x as String; 1 }, { /* x smartcasted to String */ x.length })
//
// However, as we need to fix type parameters before analyzing lambdas, this is not always the order of analysis;
// if that is possible, multiple lambdas should be considered to be concurrent.
//
// foo(run { x as String; genericFunction() }, run { /* x not smartcastable because this lambda may be resolved first */ 1 })
// /* x is smartcastable after the call */
//
// And if the lambda is conditional, then the data flow needs to be merged with other branches.
//
// foo(nullable?.let { x as String; genericFunction() }, run { 1 })
// /* x is not smartcastable */
//
// foo(nullable ?: run { x as String; genericFunction() }, run { 1 })
// /* x is not smartcastable */
//
// foo(if (condition) run { x as String; genericFunction() } else { genericFunction() }, run { 1 })
// /* x is not smartcastable */
//
// foo(if (condition) run { x as String; genericFunction() } else { x as String; genericFunction() }, run { 1 })
// /* x is smartcastable */
//
// `splitDataFlowForPostponedLambdas` in `enterX` should be matched with either `joinDataFlowFromPostponedLambdasWith`
// or `processUnionOfArguments` in `exitX`. The difference is that the latter creates an intersection of all the lambdas'
// type information (like after function calls - all casts from all lambdas are valid) while the former is a union
// (like after `if` - only the casts from one of the lambdas are valid, and we don't know which).
//
private fun splitDataFlowForPostponedLambdas() {
exitsFromCompletedPostponedAnonymousFunctions.add(mutableListOf())
}
private fun joinDataFlowFromPostponedLambdasWith(node: CFGNode<*>): MergePostponedLambdaExitsNode? {
val currentLevelExits = exitsFromCompletedPostponedAnonymousFunctions.popLast()
if (currentLevelExits.isEmpty()) {
return null
}
val joinNode = createMergePostponedLambdaExitsNode(node.fir)
addEdge(node, joinNode)
currentLevelExits.joinDataFlowFromPostponedLambdasTo(joinNode)
exitsFromCompletedPostponedAnonymousFunctions.lastOrNull()?.add(joinNode)
return joinNode
}
private fun MutableList<CFGNode<*>>.joinDataFlowFromPostponedLambdasTo(node: CFGNode<*>) {
for (exitNode in this) {
// To avoid storing nodes from subgraphs in the list, we have PostponedLambdaExitNode instead of the real
// exit node of the lambda subgraph. The latter is the previous node of the former. Everything else is
// already a join/union node in this graph.
val functionExitOrMerge = if (exitNode is PostponedLambdaExitNode) exitNode.lastPreviousNode else exitNode
addEdge(functionExitOrMerge, node, preferredKind = EdgeKind.DfgForward)
}
}
private fun processUnionOfArguments(
node: CFGNode<*>,
callCompleted: Boolean
): Pair<EdgeKind, UnionFunctionCallArgumentsNode?> {
if (!shouldPassFlowFromInplaceLambda.top()) return EdgeKind.Forward to null
var kind = EdgeKind.Forward
if (!callCompleted || exitsFromCompletedPostponedAnonymousFunctions.isEmpty()) {
val currentLevelExits = exitsFromCompletedPostponedAnonymousFunctions.popLast()
if (currentLevelExits.isEmpty()) {
return EdgeKind.Forward to null
}
val unionNode by lazy { createUnionFunctionCallArgumentsNode(node.fir) }
var hasDirectPreviousNode = false
var hasPostponedLambdas = false
val iterator = exitsFromCompletedPostponedAnonymousFunctions.iterator()
val lastPostponedLambdaExitNode = lastNode
while (iterator.hasNext()) {
val exitNode = iterator.next()
if (node.level >= exitNode.level) continue
hasPostponedLambdas = true
if (exitNode == lastPostponedLambdaExitNode) {
popAndAddEdge(node, preferredKind = EdgeKind.CfgForward)
kind = EdgeKind.DfgForward
hasDirectPreviousNode = true
if (!callCompleted || !shouldPassFlowFromInplaceLambda.top()) {
currentLevelExits.singleOrNull()?.let {
exitsFromCompletedPostponedAnonymousFunctions.lastOrNull()?.add(it)
return EdgeKind.Forward to null
}
addEdge(exitNode.lastPreviousNode, unionNode, preferredKind = EdgeKind.DfgForward)
iterator.remove()
val unionNode = createUnionFunctionCallArgumentsNode(node.fir)
currentLevelExits.joinDataFlowFromPostponedLambdasTo(unionNode)
exitsFromCompletedPostponedAnonymousFunctions.lastOrNull()?.addAll(currentLevelExits)
return EdgeKind.Forward to unionNode
}
if (hasPostponedLambdas) {
if (hasDirectPreviousNode) {
lastNodes.push(unionNode)
} else {
addNewSimpleNode(unionNode)
}
} else {
return EdgeKind.Forward to null
val unionNode = createUnionFunctionCallArgumentsNode(node.fir)
currentLevelExits.joinDataFlowFromPostponedLambdasTo(unionNode)
if (lastNode in currentLevelExits) {
popAndAddEdge(node, preferredKind = EdgeKind.CfgForward)
lastNodes.push(unionNode)
return EdgeKind.DfgForward to unionNode
}
return Pair(kind, unionNode)
addNewSimpleNode(unionNode)
return EdgeKind.Forward to unionNode
}
fun exitWhenSubjectExpression(expression: FirWhenSubjectExpression): WhenSubjectExpressionExitNode {
@@ -1290,19 +1335,21 @@ class ControlFlowGraphBuilder {
} else {
addEdge(lastNode, exitNode)
}
splitDataFlowForPostponedLambdas()
return enterNode
}
fun exitSafeCall(): ExitSafeCallNode {
fun exitSafeCall(): Pair<ExitSafeCallNode, MergePostponedLambdaExitsNode?> {
// There will be two paths towards this exit safe call node:
// one from the node prior to the enclosing safe call, and
// the other from the selector part in the enclosing safe call.
// Note that *neither* points to the safe call directly.
// So, when it comes to the real exit of the enclosing block/function,
// the safe call bound to this exit safe call node should be retrieved.
return exitSafeCallNodes.pop().also {
return exitSafeCallNodes.pop().let {
addNewSimpleNode(it)
it.updateDeadStatus()
it to joinDataFlowFromPostponedLambdasWith(it)
}
}
@@ -1310,6 +1357,7 @@ class ControlFlowGraphBuilder {
fun enterElvis(elvisExpression: FirElvisExpression) {
elvisRhsEnterNodes.push(createElvisRhsEnterNode(elvisExpression))
splitDataFlowForPostponedLambdas()
}
fun exitElvisLhs(elvisExpression: FirElvisExpression): Triple<ElvisLhsExitNode, ElvisLhsIsNotNullNode, ElvisRhsEnterNode> {
@@ -1342,12 +1390,11 @@ class ControlFlowGraphBuilder {
return Triple(lhsExitNode, lhsIsNotNullNode, rhsEnterNode)
}
fun exitElvis(): ElvisExitNode {
fun exitElvis(): Pair<ElvisExitNode, MergePostponedLambdaExitsNode?> {
val exitNode = exitElvisExpressionNodes.pop()
addNewSimpleNode(exitNode)
exitNode.updateDeadStatus()
dropPostponedLambdasForNonDeterministicCalls()
return exitNode
return exitNode to joinDataFlowFromPostponedLambdasWith(exitNode)
}
// ----------------------------------- Contract description -----------------------------------
@@ -244,6 +244,9 @@ fun ControlFlowGraphBuilder.createAnonymousObjectExpressionExitNode(fir: FirAnon
fun ControlFlowGraphBuilder.createUnionFunctionCallArgumentsNode(fir: FirElement): UnionFunctionCallArgumentsNode =
UnionFunctionCallArgumentsNode(currentGraph, fir, levelCounter, createId())
fun ControlFlowGraphBuilder.createMergePostponedLambdaExitsNode(fir: FirElement): MergePostponedLambdaExitsNode =
MergePostponedLambdaExitsNode(currentGraph, fir, levelCounter, createId())
fun ControlFlowGraphBuilder.createClassEnterNode(fir: FirClass): ClassEnterNode =
ClassEnterNode(currentGraph, fir, levelCounter, createId())
@@ -789,6 +789,7 @@ open class FirExpressionsResolveTransformer(transformer: FirBodyResolveTransform
return checkNotNullCall
}
dataFlowAnalyzer.enterCall()
checkNotNullCall.argumentList.transformArguments(transformer, ResolutionMode.ContextDependent)
checkNotNullCall.transformAnnotations(transformer, ResolutionMode.ContextIndependent)