FIR CFG: handle loops and try/catch when analyzing smartcasts in lambdas
See KT-50092
This commit is contained in:
+6
@@ -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 {
|
||||||
|
|||||||
+6
@@ -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 {
|
||||||
|
|||||||
+6
@@ -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 {
|
||||||
|
|||||||
+199
-116
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -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
|
||||||
Generated
+6
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user