FIR CFG: remove liveness analysis from lambda reassignment analyzer
Handling try/catch and break/continue is too hard here, and failing to do that properly can create incorrect smartcast. Better be conservative and assume all code is live.
This commit is contained in:
+27
-101
@@ -253,10 +253,6 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
* referenced outside the lambda. It's possible to track the scope at each node and remove the unneeded `a` in "entry" and "then"
|
* referenced outside the lambda. It's possible to track the scope at each node and remove the unneeded `a` in "entry" and "then"
|
||||||
* nodes. But doing that seems to be more expensive than simply letting it propagate.
|
* nodes. But doing that seems to be more expensive than simply letting it propagate.
|
||||||
*
|
*
|
||||||
* There is some liveness analysis present in this construction which errs on the conservative side: some code that is dead
|
|
||||||
* is considered to be live. This restricts some smart casts, which is better than the opposite (permitting unsafe smart casts
|
|
||||||
* if code turns out to be only mostly dead).
|
|
||||||
*
|
|
||||||
* Because names are not resolved at this point, we manually track local variable declarations and resolve them along the way
|
* Because names are not resolved at this point, we manually track local variable declarations and resolve them along the way
|
||||||
* so that shadowed names are handled correctly. This works because local variables at any scope have higher priority
|
* so that shadowed names are handled correctly. This works because local variables at any scope have higher priority
|
||||||
* than members on implicit receivers, even if the implicit receiver is introduced by a later scope.
|
* than members on implicit receivers, even if the implicit receiver is introduced by a later scope.
|
||||||
@@ -277,15 +273,8 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private class MiniFlow(val parents: Set<MiniFlow>) {
|
private class MiniFlow(val parents: Set<MiniFlow>) {
|
||||||
val children: MutableSet<MiniFlow> = mutableSetOf()
|
|
||||||
val assignedLater: MutableSet<FirProperty> = mutableSetOf()
|
val assignedLater: MutableSet<FirProperty> = mutableSetOf()
|
||||||
|
|
||||||
init {
|
|
||||||
for (parent in parents) {
|
|
||||||
parent.children.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fork(): MiniFlow = MiniFlow(setOf(this))
|
fun fork(): MiniFlow = MiniFlow(setOf(this))
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -305,10 +294,9 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
visitFunction(simpleFunction, data)
|
visitFunction(simpleFunction, data)
|
||||||
|
|
||||||
override fun visitFunction(function: FirFunction, data: MiniCfgData) {
|
override fun visitFunction(function: FirFunction, data: MiniCfgData) {
|
||||||
val currentFlow = data.flow ?: return
|
|
||||||
val freeVariables = data.variableDeclarations.flatMapTo(mutableSetOf()) { it.values }
|
val freeVariables = data.variableDeclarations.flatMapTo(mutableSetOf()) { it.values }
|
||||||
val flowInto = currentFlow.fork()
|
val flowInto = data.flow.fork()
|
||||||
val flowAfter = currentFlow.fork()
|
val flowAfter = data.flow.fork()
|
||||||
data.flow = flowInto
|
data.flow = flowInto
|
||||||
function.acceptChildren(this, data)
|
function.acceptChildren(this, data)
|
||||||
data.flow = flowAfter
|
data.flow = flowAfter
|
||||||
@@ -318,95 +306,50 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
|
|
||||||
override fun visitWhenExpression(whenExpression: FirWhenExpression, data: MiniCfgData) {
|
override fun visitWhenExpression(whenExpression: FirWhenExpression, data: MiniCfgData) {
|
||||||
(whenExpression.subjectVariable ?: whenExpression.subject)?.accept(this, data)
|
(whenExpression.subjectVariable ?: whenExpression.subject)?.accept(this, data)
|
||||||
val flow = data.flow ?: return
|
val flow = data.flow
|
||||||
// Also collect `flow` here for the case when none of the branches execute.
|
// Also collect `flow` here for the case when none of the branches execute.
|
||||||
val branches = whenExpression.branches.mapNotNullTo(mutableSetOf(flow)) {
|
data.flow = whenExpression.branches.mapTo(mutableSetOf(flow)) {
|
||||||
// No need to create a fork - it'll not be observed anywhere anyway.
|
// No need to create a fork - it'll not be observed anywhere anyway.
|
||||||
data.flow = flow
|
data.flow = flow
|
||||||
it.accept(this, data)
|
it.accept(this, data)
|
||||||
data.flow
|
data.flow
|
||||||
}
|
}.join()
|
||||||
data.flow = branches.join()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitTryExpression(tryExpression: FirTryExpression, data: MiniCfgData) {
|
override fun visitTryExpression(tryExpression: FirTryExpression, data: MiniCfgData) {
|
||||||
if (data.flow == null) return
|
|
||||||
tryExpression.tryBlock.accept(this, data)
|
tryExpression.tryBlock.accept(this, data)
|
||||||
val catchFlow = data.lastLiveFlow // descendant of flow at the start
|
val flow = data.flow
|
||||||
val returnFlows = mutableSetOf<MiniFlow>()
|
data.flow = tryExpression.catches.mapTo(mutableSetOf(flow)) {
|
||||||
data.flow?.let(returnFlows::add)
|
data.flow = flow
|
||||||
val finallyFlows = tryExpression.catches.mapTo(mutableSetOf(catchFlow)) {
|
|
||||||
data.flow = catchFlow
|
|
||||||
it.accept(this, data)
|
it.accept(this, data)
|
||||||
data.flow?.let(returnFlows::add)
|
data.flow
|
||||||
// Throwing/returning inside a catch clause goes through finally as well.
|
}.join()
|
||||||
data.lastLiveFlow
|
tryExpression.finallyBlock?.accept(this, data)
|
||||||
}
|
|
||||||
val finally = tryExpression.finallyBlock
|
|
||||||
if (finally != null) {
|
|
||||||
data.flow = finallyFlows.join()
|
|
||||||
finally.accept(this, data)
|
|
||||||
if (returnFlows.isEmpty()) {
|
|
||||||
data.flow = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data.flow = returnFlows.join()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Set<MiniFlow>.join(): MiniFlow? =
|
private fun Set<MiniFlow>.join(): MiniFlow =
|
||||||
when (size) {
|
singleOrNull() ?: MiniFlow(this)
|
||||||
0 -> null
|
|
||||||
1 -> single()
|
|
||||||
else -> MiniFlow(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitWhileLoop(whileLoop: FirWhileLoop, data: MiniCfgData) {
|
override fun visitWhileLoop(whileLoop: FirWhileLoop, data: MiniCfgData) {
|
||||||
// Loop entry is a merge point, so need a new node.
|
// Loop entry is a merge point, so need a new node.
|
||||||
val start = data.flow?.fork() ?: return
|
val start = data.flow.fork()
|
||||||
data.flow = start
|
data.flow = start
|
||||||
whileLoop.condition.accept(this, data)
|
whileLoop.condition.accept(this, data)
|
||||||
if (data.flow == null) return
|
|
||||||
whileLoop.block.accept(this, data)
|
whileLoop.block.accept(this, data)
|
||||||
// There may have been a conditional break/continue before the return, or the condition
|
data.flow.addBackEdgeTo(start)
|
||||||
// may have always been false.
|
|
||||||
data.flow = data.lastLiveFlow // descendant of flow after condition
|
|
||||||
data.flow?.addBackEdgeTo(start)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitDoWhileLoop(doWhileLoop: FirDoWhileLoop, data: MiniCfgData) {
|
override fun visitDoWhileLoop(doWhileLoop: FirDoWhileLoop, data: MiniCfgData) {
|
||||||
val start = data.flow?.fork() ?: return
|
val start = data.flow.fork()
|
||||||
data.flow = start
|
|
||||||
doWhileLoop.block.accept(this, data)
|
doWhileLoop.block.accept(this, data)
|
||||||
// Like above, there might have been a break/continue, so the fact that the block does not
|
|
||||||
// terminate doesn't actually mean much.
|
|
||||||
data.flow = data.lastLiveFlow // descendant of flow before the block
|
|
||||||
doWhileLoop.condition.accept(this, data)
|
doWhileLoop.condition.accept(this, data)
|
||||||
data.flow?.addBackEdgeTo(start)
|
data.flow.addBackEdgeTo(start)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitBreakExpression(breakExpression: FirBreakExpression, data: MiniCfgData) {
|
// TODO: liveness analysis - return/throw/break/continue terminate the flow.
|
||||||
visitElement(breakExpression, data)
|
// This is somewhat problematic though because try-catch and loops can restore it.
|
||||||
// Can treat this as an unconditional return if looping constructs reset the flow anyway.
|
// It is not possible to implement liveness analysis partially - otherwise combinations of
|
||||||
// TODO: check which loop this is targeting for more precise liveness analysis?
|
// control flow structures can cause incorrect smartcasts.
|
||||||
data.flow = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitContinueExpression(continueExpression: FirContinueExpression, data: MiniCfgData) {
|
|
||||||
visitElement(continueExpression, data)
|
|
||||||
// Same comment as for `break`.
|
|
||||||
data.flow = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitReturnExpression(returnExpression: FirReturnExpression, data: MiniCfgData) {
|
|
||||||
visitElement(returnExpression, data)
|
|
||||||
data.flow = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitThrowExpression(throwExpression: FirThrowExpression, data: MiniCfgData) {
|
|
||||||
visitElement(throwExpression, data)
|
|
||||||
data.flow = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitFunctionCall(functionCall: FirFunctionCall, data: MiniCfgData) {
|
override fun visitFunctionCall(functionCall: FirFunctionCall, data: MiniCfgData) {
|
||||||
val visitor = this
|
val visitor = this
|
||||||
@@ -449,40 +392,23 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
private fun MiniCfgData.recordAssignment(reference: FirReference) {
|
private fun MiniCfgData.recordAssignment(reference: FirReference) {
|
||||||
val name = (reference as? FirNamedReference)?.name ?: return
|
val name = (reference as? FirNamedReference)?.name ?: return
|
||||||
val property = variableDeclarations.lastOrNull { name in it }?.get(name) ?: return
|
val property = variableDeclarations.lastOrNull { name in it }?.get(name) ?: return
|
||||||
flow?.recordAssignment(property, mutableSetOf())
|
flow.recordAssignments(setOf(property), mutableSetOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MiniFlow.recordAssignment(property: FirProperty, visited: MutableSet<MiniFlow>) {
|
private fun MiniFlow.recordAssignments(properties: Set<FirProperty>, visited: MutableSet<MiniFlow>) {
|
||||||
if (!visited.add(this)) return
|
if (!visited.add(this)) return
|
||||||
assignedLater += property
|
assignedLater += properties
|
||||||
parents.forEach { it.recordAssignment(property, visited) }
|
parents.forEach { it.recordAssignments(properties, visited) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MiniFlow.addBackEdgeTo(loopStart: MiniFlow) {
|
private fun MiniFlow.addBackEdgeTo(loopStart: MiniFlow) {
|
||||||
children.add(loopStart)
|
|
||||||
// All forks in the loop should have the same set of variables assigned later, equal to the set
|
// All forks in the loop should have the same set of variables assigned later, equal to the set
|
||||||
// at the start of the loop.
|
// at the start of the loop.
|
||||||
propagateForward(loopStart.assignedLater, mutableSetOf())
|
recordAssignments(loopStart.assignedLater, mutableSetOf())
|
||||||
}
|
|
||||||
|
|
||||||
private fun MiniFlow.propagateForward(properties: Set<FirProperty>, visited: MutableSet<MiniFlow>) {
|
|
||||||
if (!visited.add(this)) return
|
|
||||||
assignedLater.addAll(properties)
|
|
||||||
children.forEach { it.propagateForward(properties, visited) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MiniCfgData {
|
class MiniCfgData {
|
||||||
var lastLiveFlow: MiniFlow = MiniFlow.start()
|
var flow: MiniFlow = MiniFlow.start()
|
||||||
private set
|
|
||||||
|
|
||||||
var flow: MiniFlow? = lastLiveFlow
|
|
||||||
set(value) {
|
|
||||||
if (value != null) {
|
|
||||||
lastLiveFlow = value
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
val variableDeclarations: ArrayDeque<MutableMap<Name, FirProperty>> = ArrayDeque(listOf(mutableMapOf()))
|
val variableDeclarations: ArrayDeque<MutableMap<Name, FirProperty>> = ArrayDeque(listOf(mutableMapOf()))
|
||||||
val functionForks: MutableMap<FirFunctionSymbol<*>, FunctionFork> = mutableMapOf()
|
val functionForks: MutableMap<FirFunctionSymbol<*>, FunctionFork> = mutableMapOf()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -50,6 +50,6 @@ fun test4() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
x = null
|
x = null
|
||||||
lambda?.invoke()
|
lambda<!UNNECESSARY_SAFE_CALL!>?.<!>invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user