FIR CFA: refactor handling of classes a bit

As usual, I'm leaving TODOs wherever I go...
This commit is contained in:
pyos
2022-12-09 22:44:00 +01:00
committed by Dmitriy Novozhilov
parent 4c6eff9174
commit e6819e1295
3 changed files with 110 additions and 133 deletions
@@ -13,7 +13,6 @@ import org.jetbrains.kotlin.fir.contracts.description.ConeConditionalEffectDecla
import org.jetbrains.kotlin.fir.contracts.description.ConeReturnsEffectDeclaration
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.impl.FirDefaultPropertyAccessor
import org.jetbrains.kotlin.fir.declarations.utils.isLocal
import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.expressions.impl.FirNoReceiverExpression
import org.jetbrains.kotlin.fir.references.FirControlFlowGraphReference
@@ -204,34 +203,23 @@ abstract class FirDataFlowAnalyzer(
// ----------------------------------- Classes -----------------------------------
fun enterClass() {
graphBuilder.enterClass()
fun enterClass(klass: FirClass, buildGraph: Boolean) {
graphBuilder.enterClass(klass, buildGraph)?.mergeIncomingFlow()
}
fun exitClass() {
graphBuilder.exitClass()
}
fun exitRegularClass(klass: FirRegularClass): ControlFlowGraph {
if (klass.isLocal && components.container !is FirClass) return exitLocalClass(klass)
return graphBuilder.exitClass(klass)
}
private fun exitLocalClass(klass: FirRegularClass): ControlFlowGraph {
// TODO: support capturing of mutable properties, KT-44877
val (node, controlFlowGraph) = graphBuilder.exitLocalClass(klass)
node.mergeIncomingFlow()
return controlFlowGraph
}
fun enterAnonymousObject(anonymousObject: FirAnonymousObject) {
graphBuilder.enterAnonymousObject(anonymousObject).mergeIncomingFlow()
}
fun exitAnonymousObject(anonymousObject: FirAnonymousObject): ControlFlowGraph {
val (node, controlFlowGraph) = graphBuilder.exitAnonymousObject(anonymousObject)
node.mergeIncomingFlow()
return controlFlowGraph
fun exitClass(): ControlFlowGraph? {
// TODO: support capturing of mutable properties
// var x: String?
// x = ""
// class C { init { x = maybeNull() } }
// // C will be initialized at first use - x is no longer safe to smart cast
val (node, graph) = graphBuilder.exitClass() ?: return null
if (node != null) {
node.mergeIncomingFlow()
} else {
resetReceivers() // to state before class initialization
}
return graph
}
fun exitAnonymousObjectExpression(anonymousObjectExpression: FirAnonymousObjectExpression) {
@@ -66,8 +66,6 @@ class ControlFlowGraphBuilder {
private val finallyEnterNodes: Stack<FinallyBlockEnterNode> = stackOf()
private val finallyBlocksInProgress: Stack<FinallyBlockEnterNode> = stackOf()
private val initBlockExitNodes: Stack<InitBlockExitNode> = stackOf()
private val exitSafeCallNodes: Stack<ExitSafeCallNode> = stackOf()
private val exitElvisExpressionNodes: Stack<ElvisExitNode> = stackOf()
private val elvisRhsEnterNodes: Stack<ElvisRhsEnterNode> = stackOf()
@@ -148,9 +146,7 @@ class ControlFlowGraphBuilder {
if (localFunctionNode != null) {
addEdge(localFunctionNode, enterNode)
} else {
enterToLocalClassesMembers[function.symbol]?.let {
addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward)
}
enterToLocalClassesMembers.remove(function.symbol)?.let { addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward) }
}
createFunctionExitNode(function).also {
@@ -164,14 +160,12 @@ class ControlFlowGraphBuilder {
fun exitFunction(function: FirFunction): Pair<FunctionExitNode, ControlFlowGraph> {
require(function !is FirAnonymousFunction)
val exitNode = exitTargetsForReturn.pop()
exitTargetsForTry.pop().also { assert(it == exitNode) }
popAndAddEdge(exitNode)
val graph = popGraph()
assert(exitNode == graph.exitNode)
exitTargetsForTry.pop().also {
assert(it == graph.exitNode)
}
graph.exitNode.updateDeadStatus()
return graph.exitNode as FunctionExitNode to graph
exitNode.updateDeadStatus()
return exitNode to graph
}
// ----------------------------------- Anonymous function -----------------------------------
@@ -367,22 +361,70 @@ class ControlFlowGraphBuilder {
// ----------------------------------- Classes -----------------------------------
fun enterClass() {
pushGraph(ControlFlowGraph(null, "STUB_CLASS_GRAPH", ControlFlowGraph.Kind.Stub))
}
fun enterClass(klass: FirClass, buildGraph: Boolean): CFGNode<*>? {
if (!buildGraph) {
pushGraph(ControlFlowGraph(null, "STUB_CLASS_GRAPH", ControlFlowGraph.Kind.Stub))
return null
}
fun exitClass() {
popGraph()
}
val enterNode = when {
klass is FirAnonymousObject -> createAnonymousObjectEnterNode(klass)
// Local classes are only initialized on first use, so they look pretty much like named functions:
// control flow enters here and never leaves, and assignments invalidate smart casts.
klass is FirRegularClass && klass.isLocal && currentGraph.kind.withBody -> createLocalClassExitNode(klass)
else -> null
}
fun exitClass(klass: FirClass): ControlFlowGraph {
exitClass()
val name = when (klass) {
is FirAnonymousObject -> "<anonymous object>"
is FirRegularClass -> klass.name.asString()
else -> throw IllegalArgumentException("Unknown class kind: ${klass::class}")
}
pushGraph(ControlFlowGraph(klass, name, ControlFlowGraph.Kind.ClassInitializer))
val graphEnterNode = createClassEnterNode(klass)
if (enterNode != null) {
// TODO: anonymous objects are used to represent enum entries - check what happens there
// (likely `exitClass` leaves a node on the stack that will never be consumed)
lastNodes.popOrNull()?.let { addEdge(it, enterNode) }
lastNodes.push(enterNode)
addEdge(enterNode, graphEnterNode, preferredKind = EdgeKind.CfgForward)
}
return enterNode
}
fun exitClass(): Pair<AnonymousObjectExitNode?, ControlFlowGraph>? {
if (currentGraph.kind == ControlFlowGraph.Kind.Stub) {
popGraph()
return null
}
val graph = popClassGraph()
if (graph.declaration !is FirAnonymousObject) {
return null to graph
}
val lastNode = lastNodes.pop() as AnonymousObjectEnterNode
val exitNode = createAnonymousObjectExitNode(lastNode.fir).also { lastNodes.push(it) }
// TODO: should merge data flow from members into the exit node.
// TODO: the reason for this liveness trickery is that if control flow is dead, the CFG-only edge from
// `graph.exitNode` gets magically transformed into a CFG+DFG dead edge, and `graph.exitNode` has no data
// flow information attached to it. This should be fixed as soon as data flow is made to go through the object.
if (!graph.exitNode.isDead) {
// This implies that `lastNode` is not dead either.
addEdge(graph.exitNode, exitNode, preferredKind = EdgeKind.CfgForward)
addEdge(lastNode, exitNode, preferredKind = EdgeKind.DfgForward)
} else {
addEdge(lastNode, exitNode, isDead = true)
}
return exitNode to graph
}
// Members of a class can be visited in any order, so data flow between them is unordered
// and we have to recreate the control flow after the fact.
private fun popClassGraph(): ControlFlowGraph {
assert(currentGraph.kind == ControlFlowGraph.Kind.ClassInitializer)
val klass = currentGraph.declaration as FirClass
val calledInPlace = mutableListOf<ControlFlowGraph>()
val calledLater = mutableListOf<ControlFlowGraph>()
for (declaration in klass.declarations) {
@@ -397,24 +439,26 @@ class ControlFlowGraphBuilder {
}
}
pushGraph(ControlFlowGraph(klass, name, ControlFlowGraph.Kind.ClassInitializer))
val exitNode = createClassExitNode(klass)
var node: CFGNode<*> = createClassEnterNode(klass)
var node: CFGNode<*> = currentGraph.enterNode as ClassEnterNode
var prevInitPartNode: CFGNode<*>? = null
for (graph in calledInPlace) {
createPartOfClassInitializationNode(graph.declaration as FirControlFlowGraphOwner).also {
addEdge(node, it, preferredKind = EdgeKind.CfgForward)
addEdge(it, graph.enterNode, preferredKind = EdgeKind.CfgForward)
node = graph.exitNode
if (prevInitPartNode != null) addEdge(prevInitPartNode!!, it, preferredKind = EdgeKind.DeadForward)
it.updateDeadStatus()
prevInitPartNode = it
val partNode = createPartOfClassInitializationNode(graph.declaration as FirControlFlowGraphOwner)
// TODO: if one initializer part does not terminate, deadness becomes funky here
addEdge(node, partNode, preferredKind = EdgeKind.CfgForward)
addEdge(partNode, graph.enterNode, preferredKind = EdgeKind.CfgForward)
if (prevInitPartNode != null) {
// Fake edge to make the nodes in this graph orderable without looking at subgraphs.
addEdge(prevInitPartNode, partNode, preferredKind = EdgeKind.DeadForward, propagateDeadness = false)
}
node = graph.exitNode
prevInitPartNode = partNode
}
val exitNode = createClassExitNode(klass)
addEdge(node, exitNode, preferredKind = EdgeKind.CfgForward)
if (prevInitPartNode != null) addEdge(prevInitPartNode!!, exitNode, preferredKind = EdgeKind.DeadForward)
exitNode.updateDeadStatus()
if (prevInitPartNode != null) {
addEdge(prevInitPartNode, exitNode, preferredKind = EdgeKind.DeadForward, propagateDeadness = false)
}
// TODO: Here we're assuming that the methods are called after the object is constructed, which is really not true
// (init blocks can call them). But FE1.0 did so too, hence the following code compiles and prints 0:
@@ -433,6 +477,9 @@ class ControlFlowGraphBuilder {
}
fun prepareForLocalClassMembers(members: Collection<FirDeclaration>) {
// TODO: this is called before `enterClass` so the data flow source for objects and local classes
// is not the enter node, but whichever node happens to be before it. This technically works,
// but is ugly.
members.forEachMember {
enterToLocalClassesMembers[it.symbol] = lastNodes.topOrNull()
}
@@ -444,42 +491,8 @@ class ControlFlowGraphBuilder {
}
}
fun exitLocalClass(klass: FirRegularClass): Pair<LocalClassExitNode, ControlFlowGraph> {
val graph = exitClass(klass)
val node = createLocalClassExitNode(klass).also {
addNewSimpleNodeIfPossible(it)
}
addEdge(node, graph.enterNode, preferredKind = EdgeKind.CfgForward)
return node to graph
}
fun enterAnonymousObject(anonymousObject: FirAnonymousObject): AnonymousObjectEnterNode {
val enterNode = createAnonymousObjectEnterNode(anonymousObject)
// TODO: looks like there was some problem with enum initializers that causes `lastNodes` to be empty
lastNodes.popOrNull()?.let { addEdge(it, enterNode, preferredKind = EdgeKind.Forward) }
lastNodes.push(enterNode)
enterClass()
return enterNode
}
fun exitAnonymousObject(anonymousObject: FirAnonymousObject): Pair<AnonymousObjectExitNode, ControlFlowGraph> {
val graph = exitClass(anonymousObject)
val enterNode = lastNodes.popOrNull()
if (enterNode !is AnonymousObjectEnterNode) {
throw AssertionError("anonymous object exit should be preceded by anonymous object enter, but got $enterNode")
}
val exitNode = createAnonymousObjectExitNode(anonymousObject)
// TODO: Intentionally not using anonymous object init blocks for data flow? Might've been a FE1.0 bug.
addEdge(enterNode, graph.enterNode, preferredKind = EdgeKind.CfgForward)
if (!graph.exitNode.isDead) {
addEdge(graph.exitNode, exitNode, preferredKind = EdgeKind.CfgForward)
}
addEdge(enterNode, exitNode, preferredKind = EdgeKind.DfgForward)
lastNodes.push(exitNode)
return exitNode to graph
}
fun exitAnonymousObjectExpression(anonymousObjectExpression: FirAnonymousObjectExpression): AnonymousObjectExpressionExitNode {
// TODO: what's AnonymousObjectExitNode for then?
return createAnonymousObjectExpressionExitNode(anonymousObjectExpression).also {
addNewSimpleNodeIfPossible(it)
}
@@ -554,11 +567,7 @@ class ControlFlowGraphBuilder {
val enterNode = createPropertyInitializerEnterNode(property)
val exitNode = createPropertyInitializerExitNode(property)
exitTargetsForTry.push(exitNode)
enterToLocalClassesMembers[property.symbol]?.let {
addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward)
}
enterToLocalClassesMembers.remove(property.symbol)?.let { addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward) }
lastNodes.push(enterNode)
return enterNode
}
@@ -582,11 +591,7 @@ class ControlFlowGraphBuilder {
val enterNode = createFieldInitializerEnterNode(field)
val exitNode = createFieldInitializerExitNode(field)
exitTargetsForTry.push(exitNode)
enterToLocalClassesMembers[field.symbol]?.let {
addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward)
}
enterToLocalClassesMembers.remove(field.symbol)?.let { addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward) }
lastNodes.push(enterNode)
return enterNode
}
@@ -1210,25 +1215,18 @@ class ControlFlowGraphBuilder {
fun enterInitBlock(initBlock: FirAnonymousInitializer): InitBlockEnterNode {
// TODO: questionable moment that we should pass data flow from init to init
pushGraph(ControlFlowGraph(initBlock, "init block", ControlFlowGraph.Kind.Function))
val enterNode = createInitBlockEnterNode(initBlock).also {
lastNodes.push(it)
}
val lastNode = runIf(lastNode is InitBlockExitNode) { lastNodes.pop() } ?: enterToLocalClassesMembers[initBlock.symbol]
lastNode?.let { addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward) }
createInitBlockExitNode(initBlock).also {
initBlockExitNodes.push(it)
exitTargetsForTry.push(it)
}
val enterNode = createInitBlockEnterNode(initBlock)
val exitNode = createInitBlockExitNode(initBlock)
exitTargetsForTry.push(exitNode)
enterToLocalClassesMembers.remove(initBlock.symbol)?.let { addEdge(it, enterNode, preferredKind = EdgeKind.DfgForward) }
lastNodes.push(enterNode)
return enterNode
}
fun exitInitBlock(initBlock: FirAnonymousInitializer): Pair<InitBlockExitNode, ControlFlowGraph> {
val exitNode = initBlockExitNodes.pop()
require(exitNode == exitTargetsForTry.pop())
val exitNode = exitTargetsForTry.pop() as InitBlockExitNode
popAndAddEdge(exitNode)
val graph = popGraph()
assert(graph.declaration == initBlock)
@@ -540,19 +540,14 @@ open class FirDeclarationsResolveTransformer(transformer: FirAbstractBodyResolve
regularClass: FirRegularClass,
data: ResolutionMode
): FirRegularClass {
dataFlowAnalyzer.enterClass()
dataFlowAnalyzer.enterClass(regularClass, !implicitTypeOnly)
val result = context.withRegularClass(regularClass, components) {
transformDeclarationContent(regularClass, data) as FirRegularClass
}
if (!implicitTypeOnly) {
val controlFlowGraph = dataFlowAnalyzer.exitRegularClass(result)
val controlFlowGraph = dataFlowAnalyzer.exitClass()
if (controlFlowGraph != null) {
result.replaceControlFlowGraphReference(FirControlFlowGraphReferenceImpl(controlFlowGraph))
} else {
dataFlowAnalyzer.exitClass()
}
return result
}
@@ -569,19 +564,15 @@ open class FirDeclarationsResolveTransformer(transformer: FirAbstractBodyResolve
transformer.firProviderInterceptor
)
}
if (!implicitTypeOnly && anonymousObject.controlFlowGraphReference == null) {
dataFlowAnalyzer.enterAnonymousObject(anonymousObject)
} else {
dataFlowAnalyzer.enterClass()
}
// TODO: why would there be a graph already?
val buildGraph = !implicitTypeOnly && anonymousObject.controlFlowGraphReference == null
dataFlowAnalyzer.enterClass(anonymousObject, buildGraph)
val result = context.withAnonymousObject(anonymousObject, components) {
transformDeclarationContent(anonymousObject, data) as FirAnonymousObject
}
if (!implicitTypeOnly && result.controlFlowGraphReference == null) {
val graph = dataFlowAnalyzer.exitAnonymousObject(result)
val graph = dataFlowAnalyzer.exitClass()
if (graph != null) {
result.replaceControlFlowGraphReference(FirControlFlowGraphReferenceImpl(graph))
} else {
dataFlowAnalyzer.exitClass()
}
return result
}