FIR CFG: handle loops and try/catch when analyzing smartcasts in lambdas

See KT-50092
This commit is contained in:
pyos
2022-09-30 14:29:55 +03:00
committed by teamcity
parent d054617b8e
commit a9ae1fcdf9
8 changed files with 339 additions and 116 deletions
@@ -30403,6 +30403,12 @@ public class DiagnosisCompilerTestFE10TestdataTestGenerated extends AbstractDiag
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt");
} }
@Test
@TestMetadata("capturedWithControlJumps.kt")
public void testCapturedWithControlJumps() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt");
}
@Test @Test
@TestMetadata("doWhileWithMiddleBreak.kt") @TestMetadata("doWhileWithMiddleBreak.kt")
public void testDoWhileWithMiddleBreak() throws Exception { public void testDoWhileWithMiddleBreak() throws Exception {
@@ -30403,6 +30403,12 @@ public class FirOldFrontendDiagnosticsTestGenerated extends AbstractFirDiagnosti
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt");
} }
@Test
@TestMetadata("capturedWithControlJumps.kt")
public void testCapturedWithControlJumps() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt");
}
@Test @Test
@TestMetadata("doWhileWithMiddleBreak.kt") @TestMetadata("doWhileWithMiddleBreak.kt")
public void testDoWhileWithMiddleBreak() throws Exception { public void testDoWhileWithMiddleBreak() throws Exception {
@@ -30403,6 +30403,12 @@ public class FirOldFrontendDiagnosticsWithLightTreeTestGenerated extends Abstrac
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt");
} }
@Test
@TestMetadata("capturedWithControlJumps.kt")
public void testCapturedWithControlJumps() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt");
}
@Test @Test
@TestMetadata("doWhileWithMiddleBreak.kt") @TestMetadata("doWhileWithMiddleBreak.kt")
public void testDoWhileWithMiddleBreak() throws Exception { public void testDoWhileWithMiddleBreak() throws Exception {
@@ -13,7 +13,6 @@ import org.jetbrains.kotlin.fir.declarations.utils.referredPropertySymbol
import org.jetbrains.kotlin.fir.expressions.* import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.references.FirNamedReference import org.jetbrains.kotlin.fir.references.FirNamedReference
import org.jetbrains.kotlin.fir.references.FirReference import org.jetbrains.kotlin.fir.references.FirReference
import org.jetbrains.kotlin.fir.resolve.dfa.FirLocalVariableAssignmentAnalyzer.Companion.MiniFlow.Companion.join
import org.jetbrains.kotlin.fir.symbols.impl.FirFunctionSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirFunctionSymbol
import org.jetbrains.kotlin.fir.visitors.FirVisitor import org.jetbrains.kotlin.fir.visitors.FirVisitor
import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.Name
@@ -27,7 +26,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.popLast
* queries after the traversal is done. * queries after the traversal is done.
**/ **/
internal class FirLocalVariableAssignmentAnalyzer( internal class FirLocalVariableAssignmentAnalyzer(
private val assignedLocalVariablesByFunction: Map<FirFunctionSymbol<*>, AssignedLocalVariables> private val assignedLocalVariablesByFunction: Map<FirFunctionSymbol<*>, FunctionFork>
) { ) {
/** /**
* Stack storing concurrent lambda arguments for the current visited anonymous function. For example * Stack storing concurrent lambda arguments for the current visited anonymous function. For example
@@ -63,7 +62,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
*/ */
private val ephemeralConcurrentlyAssignedLocalVariables: MutableSet<FirProperty> = mutableSetOf() private val ephemeralConcurrentlyAssignedLocalVariables: MutableSet<FirProperty> = mutableSetOf()
private val functionStack = mutableListOf<AssignedLocalVariables>() private val functionStack = mutableListOf<FunctionFork>()
/** Checks whether the given access is an unstable access to a local variable at this moment. */ /** Checks whether the given access is an unstable access to a local variable at this moment. */
fun isAccessToUnstableLocalVariable(qualifiedAccessExpression: FirQualifiedAccessExpression): Boolean { fun isAccessToUnstableLocalVariable(qualifiedAccessExpression: FirQualifiedAccessExpression): Boolean {
@@ -76,7 +75,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
// always null and hence there is no need to check it. In addition, since multiple lambda can be passed, we accumulate the // always null and hence there is no need to check it. In addition, since multiple lambda can be passed, we accumulate the
// effects by appending to `ephemeralConcurrentlyAssignedLocalVariables`. After the function call is resolved, // effects by appending to `ephemeralConcurrentlyAssignedLocalVariables`. After the function call is resolved,
// `exitAnonymousFunction` will be invoked at some point to properly set up the `persistentConcurrentlyAssignedLocalVariables`. // `exitAnonymousFunction` will be invoked at some point to properly set up the `persistentConcurrentlyAssignedLocalVariables`.
assignedLocalVariablesByFunction[anonymousFunction.symbol]?.insideLocalFunction?.let { assignedLocalVariablesByFunction[anonymousFunction.symbol]?.assignedInside?.let {
ephemeralConcurrentlyAssignedLocalVariables.addAll(it) ephemeralConcurrentlyAssignedLocalVariables.addAll(it)
} }
} }
@@ -98,7 +97,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
for (concurrentLambdas in concurrentLambdaArgsStack) { for (concurrentLambdas in concurrentLambdaArgsStack) {
for (otherLambda in concurrentLambdas) { for (otherLambda in concurrentLambdas) {
if (otherLambda != function && otherLambda.invocationKind != EventOccurrencesRange.ZERO) { if (otherLambda != function && otherLambda.invocationKind != EventOccurrencesRange.ZERO) {
assignedLocalVariablesByFunction[otherLambda.symbol]?.insideLocalFunction?.let { assignedLocalVariablesByFunction[otherLambda.symbol]?.assignedInside?.let {
concurrentlyAssignedLocalVariables += it concurrentlyAssignedLocalVariables += it
} }
} }
@@ -111,11 +110,11 @@ internal class FirLocalVariableAssignmentAnalyzer(
// The function may be called twice concurrently in an SMT environment, which means any assignment it executes // The function may be called twice concurrently in an SMT environment, which means any assignment it executes
// might in theory happen in between any check it does and a subsequent use of the variable. So if this function // might in theory happen in between any check it does and a subsequent use of the variable. So if this function
// does any assignments, it cannot smartcast the target variables. // does any assignments, it cannot smartcast the target variables.
concurrentlyAssignedLocalVariables += it.insideLocalFunction concurrentlyAssignedLocalVariables += it.assignedInside
// The function may also stored and called later, so assignments done outside its scope after the definition // The function may also be stored and called later, so assignments done outside its scope after the definition
// might also have executed. // might also have executed.
for (outerScope in functionStack) { for (outerScope in functionStack) {
concurrentlyAssignedLocalVariables += outerScope.outsideLocalFunction concurrentlyAssignedLocalVariables += outerScope.assignedLater
} }
} }
} }
@@ -141,7 +140,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
// } // }
// FE1.0 has the same behavior. // FE1.0 has the same behavior.
for (outerScope in concurrentlyAssignedLocalVariablesStack) { for (outerScope in concurrentlyAssignedLocalVariablesStack) {
outerScope += it.insideLocalFunction outerScope += it.assignedInside
} }
} }
} }
@@ -184,14 +183,9 @@ internal class FirLocalVariableAssignmentAnalyzer(
* *
* # Note on implementation detail * # Note on implementation detail
* *
* The analyzer constructs a mini control flow graph that captures forking of execution path. Any conditional branches, declaration of * The analyzer constructs a mini control flow graph that captures forking of execution path. The only information it cares about,
* lambda and local functions are forks. The mini CFG does not care about loop structures and effectively treats it as a linear sequence of * though, is which variables are assigned in a given node or any transitive successor; thus nodes which add no extra information
* statements. This is sufficient for the purpose of collecting unstable local variables. Similarly for try/catch/finally constructs. * and have only one predecessor are elided from the resulting graph.
*
* Also, for simplicity, all conditionals are treated as non-exhaustive. Hence, a fallback edge is always added along a conditional
* structure.
*
* While building the mini CFG, inside each node, we collect local variables that are assigned later in the execution path.
* *
* For example, consider the following code. * For example, consider the following code.
* *
@@ -216,130 +210,201 @@ internal class FirLocalVariableAssignmentAnalyzer(
* *
* The generated mini CFG looks like the following, with assigned local variables annotated after each node in curly brackets. * The generated mini CFG looks like the following, with assigned local variables annotated after each node in curly brackets.
* *
* ┌───────┐ * ┌───────┐
* if │ {x y z a} * │ entry │ {x y z a}
* └─┬─┬─┬─┘ * └─┬─┬─┬─┘
* │ │ │ fallback * │ │ │ fallback
* │ │ └────────────────────────────────────── * │ │ └─────────────────────────────┐
* │ │ false * │ │ false │
* │ └─────────────────────────┐ * │ └─────────────────────────┐ │
* │ true │ * │ true │ │
* ┌─┴─────┐ ┌───┴────┐ * ┌ ┴ ─ ─ ┐ ┌ ─ ┴ ─ ┐
* run │ {x z a} else │ {x y z} * │ then │ │ else │ │
* │ branch │ * └ ┬ ─ ┬ ┘ └ ─ ┬ ─ ┘
* └─┬───┬─┘ └───┬────┘ * │ │ normal execution │
* normal execution │ * └─────────────┐
* └─────────────┐ │ │ * │ lambda arg
* │ lambda arg │ │ * ┌─┴──────┐ ┌───┴───┐ │
* ┌─┴──────┐ ┌───┴───┐ │ * │ lambda │ {x} │ empty │ {z} │
* │ lambda │ {x} │ empty │ {z} │ * └────────┘ └───┬───┘
* body │ │ * ┌─────────────────┘ │ │
* └────────┘ └──────┘ │ * │ ┌─────────────────────────┘ │
* ─────────────────┘ │ │ * │ │ ┌─────────────────────────────┘
* ┌─────────────────────────┘ │ * ┌─┴─┴─┴─┐
* │ │ ┌──────────────────────────────────────┘ * │ after │ {z}
* ┌─┴─┴─┴─┐ * │ if │
* │ after │ {z} * └───────┘
* │ if │
* └───────┘
* *
* Some notes on why each node contains what it contains: * Some notes on why each node contains what it contains:
* - the nodes with the dashed outline and no associated set, "then" and "else", are elided, as they are neither merge points
* nor do they have any useful information - we will never need to know which variables are assigned to after entering the
* "else" branch, for example, because that code path will encounter no lambdas. Instead, any assignments done in elided
* blocks are immediately propagated to the parents, which is why the "entry" node contains `y`.
*
* - the "lambda" and "empty" nodes are not merge points, but they are what the CFG is for: if the lambda is not called-in-place,
* then all variables it assigns to cannot be smartcasted in "empty" or its successors and vice versa, as the body of the lambda
* can be executed at arbitrary points on that path.
* *
* - changes to `z` is captured and back-propagated to all earlier nodes as desired. * - changes to `z` is captured and back-propagated to all earlier nodes as desired.
* *
* - "lambda body" node does not contain `a` because `a` is declared inside the function. Such declarations are removed in * - "lambda body" node does not contain `a` because `a` is declared inside the function. Such declarations are removed after
* [MiniCfgBuilder.handleFunctionFork] after the lambda function is processed. However, the parent nodes contain `a` because * the graph is constructed, as loop closure can reintroduce them at any point. However, the parent nodes contain `a` because
* [MiniCfgBuilder.recordAssignment] propagates `a` during traversal. The extra `a` won't do any harm since `a` can never be * [MiniCfgBuilder.recordAssignment] propagates `a` during traversal. The extra `a` won't do any harm since `a` can never be
* referenced outside the lambda. It's possible to track the scope at each node and remove the unneeded `a` in "if" and "run" * 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.
* *
* - "run" node does not contain `y` as desired since the if true and false branches are mutually exclusive. * 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).
* *
* By the way, since local variables are not resolved at this point, we manually track local variable declarations and resolve them along * Because names are not resolved at this point, we manually track local variable declarations and resolve them along the way
* the way so that shadowed names are handled correctly. * 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.
*/ */
fun analyzeFunction(rootFunction: FirFunction): FirLocalVariableAssignmentAnalyzer { fun analyzeFunction(rootFunction: FirFunction): FirLocalVariableAssignmentAnalyzer {
return FirLocalVariableAssignmentAnalyzer(computeAssignedLocalVariables(rootFunction)) val data = MiniCfgBuilder.MiniCfgData()
MiniCfgBuilder().visitElement(rootFunction, data)
for (fork in data.functionForks.values) {
fork.assignedInside.retainAll(fork.declaredBefore)
}
return FirLocalVariableAssignmentAnalyzer(data.functionForks)
} }
/** class FunctionFork(
* Computes a mini CFG and returns the map tracking assigned local variables at each potentially concurrent local/lambda function. val declaredBefore: Set<FirProperty>,
*/ val assignedLater: Set<FirProperty>,
private fun computeAssignedLocalVariables(firFunction: FirFunction): Map<FirFunctionSymbol<*>, AssignedLocalVariables> { val assignedInside: MutableSet<FirProperty>,
val startFlow = MiniFlow.start() )
val data = MiniCfgBuilder.MiniCfgData(startFlow)
MiniCfgBuilder().visitElement(firFunction, data)
return data.localFunctionToAssignedLocalVariables
}
class AssignedLocalVariables(val outsideLocalFunction: Set<FirProperty>, val insideLocalFunction: Set<FirProperty>)
private class MiniFlow(val parents: Set<MiniFlow>) { private class MiniFlow(val parents: Set<MiniFlow>) {
val assignedLocalVariables: MutableSet<FirProperty> = mutableSetOf() val children: MutableSet<MiniFlow> = 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 {
fun start() = MiniFlow(emptySet()) fun start() = MiniFlow(emptySet())
fun Set<MiniFlow>.join(): MiniFlow = MiniFlow(this)
} }
} }
private class MiniCfgBuilder : FirVisitor<Unit, MiniCfgBuilder.MiniCfgData>() { private class MiniCfgBuilder : FirVisitor<Unit, MiniCfgBuilder.MiniCfgData>() {
override fun visitElement(element: FirElement, data: MiniCfgData) { override fun visitElement(element: FirElement, data: MiniCfgData) {
element.acceptChildren(this, data) element.acceptChildren(this, data)
} }
override fun visitAnonymousFunction(anonymousFunction: FirAnonymousFunction, data: MiniCfgData) { override fun visitAnonymousFunction(anonymousFunction: FirAnonymousFunction, data: MiniCfgData) =
handleFunctionFork(anonymousFunction, data) visitFunction(anonymousFunction, data)
}
override fun visitSimpleFunction(simpleFunction: FirSimpleFunction, data: MiniCfgData) { override fun visitSimpleFunction(simpleFunction: FirSimpleFunction, data: MiniCfgData) =
handleFunctionFork(simpleFunction, data) visitFunction(simpleFunction, data)
}
private fun handleFunctionFork(function: FirFunction, data: MiniCfgData) { override fun visitFunction(function: FirFunction, data: MiniCfgData) {
val currentFlow = data.flow ?: return val currentFlow = data.flow ?: return
val functionFork = currentFlow.fork() val freeVariables = data.variableDeclarations.flatMapTo(mutableSetOf()) { it.values }
data.flow = functionFork val flowInto = currentFlow.fork()
val flowAfter = currentFlow.fork()
data.flow = flowInto
function.acceptChildren(this, data) function.acceptChildren(this, data)
data.flow = flowAfter
// Only retain local variables declared above the current scope. This way, any local variables declared inside the data.functionForks[function.symbol] =
// function will effectively be treated as distinct variables and, hence, stable (Of course, for nested lambda, things would FunctionFork(freeVariables, flowAfter.assignedLater, flowInto.assignedLater)
// just work because inside the lambda assigned local variables are tracked by different nodes).
functionFork.assignedLocalVariables.retainAll(data.variableDeclarations.flatMapTo(mutableSetOf()) { it.values })
// Create another fork for the normal execution
val normalExecution = currentFlow.fork()
data.localFunctionToAssignedLocalVariables[function.symbol] =
AssignedLocalVariables(normalExecution.assignedLocalVariables, functionFork.assignedLocalVariables)
data.flow = normalExecution
} }
override fun visitWhenExpression(whenExpression: FirWhenExpression, data: MiniCfgData) { override fun visitWhenExpression(whenExpression: FirWhenExpression, data: MiniCfgData) {
(whenExpression.subjectVariable ?: whenExpression.subject)?.accept(this, data)
val flow = data.flow ?: return val flow = data.flow ?: return
val visitor = this // Also collect `flow` here for the case when none of the branches execute.
with(whenExpression) { val branches = whenExpression.branches.mapNotNullTo(mutableSetOf(flow)) {
calleeReference.accept(visitor, data) // No need to create a fork - it'll not be observed anywhere anyway.
val subjectVariable = this.subjectVariable data.flow = flow
if (subjectVariable != null) { it.accept(this, data)
subjectVariable.accept(visitor, data) data.flow
} else { }
subject?.accept(visitor, data) data.flow = branches.join()
}
override fun visitTryExpression(tryExpression: FirTryExpression, data: MiniCfgData) {
if (data.flow == null) return
tryExpression.tryBlock.accept(this, data)
val catchFlow = data.lastLiveFlow // descendant of flow at the start
val returnFlows = mutableSetOf<MiniFlow>()
data.flow?.let(returnFlows::add)
val finallyFlows = tryExpression.catches.mapTo(mutableSetOf(catchFlow)) {
data.flow = catchFlow
it.accept(this, data)
data.flow?.let(returnFlows::add)
// Throwing/returning inside a catch clause goes through finally as well.
data.lastLiveFlow
}
val finally = tryExpression.finallyBlock
if (finally != null) {
data.flow = finallyFlows.join()
finally.accept(this, data)
if (returnFlows.isEmpty()) {
data.flow = null
} }
val childFlows = branches.mapNotNull { } else {
data.flow = flow.fork() data.flow = returnFlows.join()
it.accept(visitor, data)
data.flow
}
// Also collect `flow` here for the synthetic fallback flow when none of the branch executes.
data.flow = (childFlows + flow).toSet().join()
} }
} }
private fun Set<MiniFlow>.join(): MiniFlow? =
when (size) {
0 -> null
1 -> single()
else -> MiniFlow(this)
}
override fun visitWhileLoop(whileLoop: FirWhileLoop, data: MiniCfgData) {
// Loop entry is a merge point, so need a new node.
val start = data.flow?.fork() ?: return
data.flow = start
whileLoop.condition.accept(this, data)
if (data.flow == null) return
whileLoop.block.accept(this, data)
// There may have been a conditional break/continue before the return, or the condition
// 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) {
val start = data.flow?.fork() ?: return
data.flow = start
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)
data.flow?.addBackEdgeTo(start)
}
override fun visitBreakExpression(breakExpression: FirBreakExpression, data: MiniCfgData) {
visitElement(breakExpression, data)
// Can treat this as an unconditional return if looping constructs reset the flow anyway.
// TODO: check which loop this is targeting for more precise liveness analysis?
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) { override fun visitReturnExpression(returnExpression: FirReturnExpression, data: MiniCfgData) {
super.visitReturnExpression(returnExpression, data) visitElement(returnExpression, data)
// TODO: consider to also handle `throw`, which would require keeping track of all `try`, `catch` and `finally` constructs. data.flow = null
}
override fun visitThrowExpression(throwExpression: FirThrowExpression, data: MiniCfgData) {
visitElement(throwExpression, data)
data.flow = null data.flow = null
} }
@@ -357,51 +422,69 @@ internal class FirLocalVariableAssignmentAnalyzer(
override fun visitBlock(block: FirBlock, data: MiniCfgData) { override fun visitBlock(block: FirBlock, data: MiniCfgData) {
data.variableDeclarations.addLast(mutableMapOf()) data.variableDeclarations.addLast(mutableMapOf())
super.visitBlock(block, data) visitElement(block, data)
data.variableDeclarations.removeLast() data.variableDeclarations.removeLast()
} }
override fun visitProperty(property: FirProperty, data: MiniCfgData) { override fun visitProperty(property: FirProperty, data: MiniCfgData) {
super.visitProperty(property, data) visitElement(property, data)
if (property.isLocal) { if (property.isLocal) {
data.variableDeclarations.last()[property.name] = property data.variableDeclarations.last()[property.name] = property
} }
} }
override fun visitVariableAssignment(variableAssignment: FirVariableAssignment, data: MiniCfgData) { override fun visitVariableAssignment(variableAssignment: FirVariableAssignment, data: MiniCfgData) {
super.visitVariableAssignment(variableAssignment, data) visitElement(variableAssignment, data)
if (variableAssignment.explicitReceiver != null) return if (variableAssignment.explicitReceiver != null) return
data.recordAssignment(variableAssignment.lValue) data.recordAssignment(variableAssignment.lValue)
} }
override fun visitAssignmentOperatorStatement(assignmentOperatorStatement: FirAssignmentOperatorStatement, data: MiniCfgData) { override fun visitAssignmentOperatorStatement(assignmentOperatorStatement: FirAssignmentOperatorStatement, data: MiniCfgData) {
super.visitAssignmentOperatorStatement(assignmentOperatorStatement, data) visitElement(assignmentOperatorStatement, data)
val lhs = assignmentOperatorStatement.leftArgument as? FirQualifiedAccessExpression ?: return val lhs = assignmentOperatorStatement.leftArgument as? FirQualifiedAccessExpression ?: return
if (lhs.explicitReceiver != null) return if (lhs.explicitReceiver != null) return
data.recordAssignment(lhs.calleeReference) data.recordAssignment(lhs.calleeReference)
} }
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 = resolveLocalVariable(name) ?: return val property = variableDeclarations.lastOrNull { name in it }?.get(name) ?: return
flow?.recordAssignment(property, mutableSetOf()) flow?.recordAssignment(property, mutableSetOf())
} }
private fun MiniFlow.recordAssignment(property: FirProperty, visited: MutableSet<MiniFlow>) { private fun MiniFlow.recordAssignment(property: FirProperty, visited: MutableSet<MiniFlow>) {
if (this in visited) return if (!visited.add(this)) return
visited += this assignedLater += property
assignedLocalVariables += property
// Back-propagate the assignment to all parent flows.
parents.forEach { it.recordAssignment(property, visited) } parents.forEach { it.recordAssignment(property, visited) }
} }
class MiniCfgData(var flow: MiniFlow?) { private fun MiniFlow.addBackEdgeTo(loopStart: MiniFlow) {
val variableDeclarations: ArrayDeque<MutableMap<Name, FirProperty>> = ArrayDeque(listOf(mutableMapOf())) children.add(loopStart)
val localFunctionToAssignedLocalVariables: MutableMap<FirFunctionSymbol<*>, AssignedLocalVariables> = mutableMapOf() // All forks in the loop should have the same set of variables assigned later, equal to the set
// at the start of the loop.
propagateForward(loopStart.assignedLater, mutableSetOf())
}
fun resolveLocalVariable(name: Name): FirProperty? { private fun MiniFlow.propagateForward(properties: Set<FirProperty>, visited: MutableSet<MiniFlow>) {
return variableDeclarations.lastOrNull { name in it }?.getValue(name) if (!visited.add(this)) return
} assignedLater.addAll(properties)
children.forEach { it.propagateForward(properties, visited) }
}
class MiniCfgData {
var lastLiveFlow: 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 functionForks: MutableMap<FirFunctionSymbol<*>, FunctionFork> = mutableMapOf()
} }
} }
} }
@@ -0,0 +1,55 @@
// ISSUE: KT-50092
fun test1() {
var x: String? = "..."
var lambda: (() -> Int)? = null
for (i in 1..2) {
when (i) {
2 -> x = null
1 -> if (x != null) lambda = { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
}
}
lambda?.invoke()
}
fun test2() {
var x: String? = "..."
if (x != null) {
val lambda = { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
while (false) return
x = null
lambda()
}
}
fun test3() {
var x: String? = "asd"
if (x != null) {
var lambda: (() -> Int)? = null
try {
lambda = { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
if (true) throw RuntimeException("...")
return
} catch (e: Exception) {
x = null
lambda?.invoke()
} finally {
x = null
lambda?.invoke()
}
}
}
fun test4() {
var x: String? = "..."
if (x != null) {
var lambda: (() -> Int)? = null
while (true) {
lambda = { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
if (true) break
return
}
x = null
lambda?.invoke()
}
}
@@ -0,0 +1,55 @@
// ISSUE: KT-50092
fun test1() {
var x: String? = "..."
var lambda: (() -> Int)? = null
for (i in 1..2) {
when (i) {
2 -> x = null
1 -> if (x != null) lambda = { <!DEBUG_INFO_SMARTCAST!>x<!>.length } // bad
}
}
lambda?.invoke()
}
fun test2() {
var x: String? = "..."
if (x != null) {
val lambda = { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
while (false) return
x = null
lambda()
}
}
fun test3() {
var x: String? = "asd"
if (x != null) {
var lambda: (() -> Int)? = null
try {
lambda = { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
if (true) throw RuntimeException("...")
return
} catch (e: Exception) {
x = null
lambda?.invoke()
} finally {
x = null
lambda?.invoke()
}
}
}
fun test4() {
var x: String? = "..."
if (x != null) {
var lambda: (() -> Int)? = <!VARIABLE_WITH_REDUNDANT_INITIALIZER!>null<!>
while (true) {
lambda = { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
if (true) break
return
}
x = null
lambda?.invoke()
}
}
@@ -0,0 +1,6 @@
package
public fun test1(): kotlin.Unit
public fun test2(): kotlin.Unit
public fun test3(): kotlin.Unit
public fun test4(): kotlin.Unit
@@ -30493,6 +30493,12 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest {
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByNested.kt");
} }
@Test
@TestMetadata("capturedWithControlJumps.kt")
public void testCapturedWithControlJumps() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt");
}
@Test @Test
@TestMetadata("doWhileWithMiddleBreak.kt") @TestMetadata("doWhileWithMiddleBreak.kt")
public void testDoWhileWithMiddleBreak() throws Exception { public void testDoWhileWithMiddleBreak() throws Exception {