FIR CFG: process called-in-place lambdas as loops

This commit is contained in:
pyos
2022-06-16 12:58:55 +02:00
committed by Dmitriy Novozhilov
parent 8214e4f806
commit 06c7572ee5
16 changed files with 116 additions and 31 deletions
@@ -30497,6 +30497,12 @@ public class DiagnosisCompilerTestFE10TestdataTestGenerated extends AbstractDiag
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt");
} }
@Test
@TestMetadata("leakingLambdaInCalledInPlace.kt")
public void testLeakingLambdaInCalledInPlace() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/leakingLambdaInCalledInPlace.kt");
}
@Test @Test
@TestMetadata("nestedDoWhile.kt") @TestMetadata("nestedDoWhile.kt")
public void testNestedDoWhile() throws Exception { public void testNestedDoWhile() throws Exception {
@@ -30497,6 +30497,12 @@ public class FirOldFrontendDiagnosticsTestGenerated extends AbstractFirDiagnosti
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt");
} }
@Test
@TestMetadata("leakingLambdaInCalledInPlace.kt")
public void testLeakingLambdaInCalledInPlace() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/leakingLambdaInCalledInPlace.kt");
}
@Test @Test
@TestMetadata("nestedDoWhile.kt") @TestMetadata("nestedDoWhile.kt")
public void testNestedDoWhile() throws Exception { public void testNestedDoWhile() throws Exception {
@@ -30497,6 +30497,12 @@ public class FirOldFrontendDiagnosticsWithLightTreeTestGenerated extends Abstrac
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt");
} }
@Test
@TestMetadata("leakingLambdaInCalledInPlace.kt")
public void testLeakingLambdaInCalledInPlace() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/leakingLambdaInCalledInPlace.kt");
}
@Test @Test
@TestMetadata("nestedDoWhile.kt") @TestMetadata("nestedDoWhile.kt")
public void testNestedDoWhile() throws Exception { public void testNestedDoWhile() throws Exception {
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.fir.resolve.dfa package org.jetbrains.kotlin.fir.resolve.dfa
import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange
import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.fir.* import org.jetbrains.kotlin.fir.*
import org.jetbrains.kotlin.fir.contracts.FirResolvedContractDescription import org.jetbrains.kotlin.fir.contracts.FirResolvedContractDescription
@@ -284,6 +285,13 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
// TODO: questionable // TODO: questionable
postponedLambdaEnterNode?.mergeIncomingFlow() postponedLambdaEnterNode?.mergeIncomingFlow()
functionEnterNode.mergeIncomingFlow(shouldForkFlow = true) functionEnterNode.mergeIncomingFlow(shouldForkFlow = true)
when (anonymousFunction.invocationKind) {
EventOccurrencesRange.AT_LEAST_ONCE,
EventOccurrencesRange.MORE_THAN_ONCE,
EventOccurrencesRange.UNKNOWN, null ->
enterCapturingStatement(functionEnterNode, anonymousFunction)
else -> {}
}
logicSystem.updateAllReceivers(functionEnterNode.flow) logicSystem.updateAllReceivers(functionEnterNode.flow)
} }
@@ -292,6 +300,13 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
anonymousFunction anonymousFunction
) )
val (functionExitNode, postponedLambdaExitNode, graph) = graphBuilder.exitAnonymousFunction(anonymousFunction) val (functionExitNode, postponedLambdaExitNode, graph) = graphBuilder.exitAnonymousFunction(anonymousFunction)
when (anonymousFunction.invocationKind) {
EventOccurrencesRange.AT_LEAST_ONCE,
EventOccurrencesRange.MORE_THAN_ONCE,
EventOccurrencesRange.UNKNOWN, null ->
exitCapturingStatement(anonymousFunction)
else -> {}
}
// TODO: questionable // TODO: questionable
postponedLambdaExitNode?.mergeIncomingFlow() postponedLambdaExitNode?.mergeIncomingFlow()
functionExitNode.mergeIncomingFlow() functionExitNode.mergeIncomingFlow()
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.fir.resolve.dfa package org.jetbrains.kotlin.fir.resolve.dfa
import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange
import org.jetbrains.kotlin.contracts.description.isInPlace
import org.jetbrains.kotlin.fir.FirElement import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.declarations.* import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.utils.referredPropertySymbol import org.jetbrains.kotlin.fir.declarations.utils.referredPropertySymbol
@@ -61,6 +62,8 @@ internal class FirLocalVariableAssignmentAnalyzer(
*/ */
private val ephemeralConcurrentlyAssignedLocalVariables: MutableSet<FirProperty> = mutableSetOf() private val ephemeralConcurrentlyAssignedLocalVariables: MutableSet<FirProperty> = mutableSetOf()
private val functionStack = mutableListOf<AssignedLocalVariables>()
/** 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 {
val property = qualifiedAccessExpression.referredPropertySymbol?.fir ?: return false val property = qualifiedAccessExpression.referredPropertySymbol?.fir ?: return false
@@ -101,23 +104,27 @@ internal class FirLocalVariableAssignmentAnalyzer(
} }
} }
when ((function as? FirAnonymousFunction)?.invocationKind) { assignedLocalVariablesByFunction[function.symbol]?.let {
EventOccurrencesRange.AT_LEAST_ONCE, functionStack.add(it)
EventOccurrencesRange.MORE_THAN_ONCE -> if (function !is FirAnonymousFunction || !function.invocationKind.isInPlace) {
// The function may be called repeatedly so the assignments may have already executed before we enter it again. // The function may be called twice concurrently in an SMT environment, which means any assignment it executes
assignedLocalVariablesByFunction[function.symbol]?.insideLocalFunction?.let { concurrentlyAssignedLocalVariables += it } // might in theory happen in between any check it does and a subsequent use of the variable. So if this function
EventOccurrencesRange.UNKNOWN, null -> // does any assignments, it cannot smartcast the target variables.
// The function may not only be called repeatedly, but also stored and called later, so assignments done outside concurrentlyAssignedLocalVariables += it.insideLocalFunction
// its scope after the definition might also have executed. // The function may also stored and called later, so assignments done outside its scope after the definition
assignedLocalVariablesByFunction[function.symbol]?.all?.let { concurrentlyAssignedLocalVariables += it } // might also have executed.
else -> {} // The function is called at most once so its assignments have not executed yet. for (outerScope in functionStack) {
concurrentlyAssignedLocalVariables += outerScope.outsideLocalFunction
}
}
} }
} }
fun exitLocalFunction(function: FirFunction) { fun exitLocalFunction(function: FirFunction) {
concurrentlyAssignedLocalVariablesStack.removeLast() concurrentlyAssignedLocalVariablesStack.removeLast()
when ((function as? FirAnonymousFunction)?.invocationKind) { assignedLocalVariablesByFunction[function.symbol]?.let {
EventOccurrencesRange.UNKNOWN, null -> functionStack.popLast()
if (function !is FirAnonymousFunction || !function.invocationKind.isInPlace) {
// The function may be stored and then called later, so any access to the variables it touches // The function may be stored and then called later, so any access to the variables it touches
// is no longer smartcastable ever. // is no longer smartcastable ever.
// //
@@ -132,12 +139,10 @@ internal class FirLocalVariableAssignmentAnalyzer(
// p.memberOfSomething // Bad // p.memberOfSomething // Bad
// } // }
// FE1.0 has the same behavior. // FE1.0 has the same behavior.
assignedLocalVariablesByFunction[function.symbol]?.insideLocalFunction?.let { for (outerScope in concurrentlyAssignedLocalVariablesStack) {
for (outerScope in concurrentlyAssignedLocalVariablesStack) { outerScope += it.insideLocalFunction
outerScope += it
}
} }
else -> {} // The function is only called inline; this is handled by CFG construction by visiting the function body. }
} }
} }
@@ -266,9 +271,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
return data.localFunctionToAssignedLocalVariables return data.localFunctionToAssignedLocalVariables
} }
class AssignedLocalVariables(val outsideLocalFunction: Set<FirProperty>, val insideLocalFunction: Set<FirProperty>) { class AssignedLocalVariables(val outsideLocalFunction: Set<FirProperty>, val insideLocalFunction: Set<FirProperty>)
val all get() = outsideLocalFunction + insideLocalFunction
}
private class MiniFlow(val parents: Set<MiniFlow>) { private class MiniFlow(val parents: Set<MiniFlow>) {
val assignedLocalVariables: MutableSet<FirProperty> = mutableSetOf() val assignedLocalVariables: MutableSet<FirProperty> = mutableSetOf()
@@ -304,7 +307,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
// Only retain local variables declared above the current scope. This way, any local variables declared inside the // Only retain local variables declared above the current scope. This way, any local variables declared inside the
// function will effectively be treated as distinct variables and, hence, stable (Of course, for nested lambda, things would // function will effectively be treated as distinct variables and, hence, stable (Of course, for nested lambda, things would
// just work because inside the lambda assigned local variables are tracked by different nodes). // just work because inside the lambda assigned local variables are tracked by different nodes).
functionFork.assignedLocalVariables.retainAll(data.variableDeclarations.flatMap { it.values }) functionFork.assignedLocalVariables.retainAll(data.variableDeclarations.flatMapTo(mutableSetOf()) { it.values })
// Create another fork for the normal execution // Create another fork for the normal execution
val normalExecution = currentFlow.fork() val normalExecution = currentFlow.fork()
data.localFunctionToAssignedLocalVariables[function.symbol] = data.localFunctionToAssignedLocalVariables[function.symbol] =
@@ -28,7 +28,7 @@ fun test() {
var s: String? = null var s: String? = null
s = "" s = ""
atLeastOnce { atLeastOnce {
<!SMARTCAST_IMPOSSIBLE!>s<!>.length // unstable since lambda can be called twice s<!UNSAFE_CALL!>.<!>length // unstable since lambda can be called twice
s = null s = null
var s2: String? = null var s2: String? = null
s2 = "" s2 = ""
@@ -30,7 +30,7 @@ fun baz(s: String?) {
<!SMARTCAST_IMPOSSIBLE!>x<!>.hashCode() <!SMARTCAST_IMPOSSIBLE!>x<!>.hashCode()
} }
run { run {
<!SMARTCAST_IMPOSSIBLE!>x<!>.hashCode() x<!UNSAFE_CALL!>.<!>hashCode()
x = null x = null
} }
} }
@@ -40,7 +40,7 @@ fun gaz(s: String?) {
var x = s var x = s
if (x != null) { if (x != null) {
run { run {
<!SMARTCAST_IMPOSSIBLE!>x<!>.hashCode() x<!UNSAFE_CALL!>.<!>hashCode()
x = null x = null
} }
run { run {
@@ -0,0 +1,11 @@
// FIR_IDENTICAL
fun main() {
var p: String?
var block: () -> Int = { 1 }
p = "2"
run {
block = { <!SMARTCAST_IMPOSSIBLE!>p<!>.length }
}
p = null
block()
}
@@ -0,0 +1,3 @@
package
public fun main(): kotlin.Unit
@@ -8,7 +8,8 @@ public fun foo() {
} else if (s == null) { } else if (s == null) {
return -2 return -2
} else { } else {
return <!SMARTCAST_IMPOSSIBLE!>s<!>.length // Here smartcast is possible, at least in principle // Smart cast might be unsafe if function is invoked twice concurrently
return <!SMARTCAST_IMPOSSIBLE!>s<!>.length
} }
} }
if (s != null) { if (s != null) {
@@ -0,0 +1,16 @@
// See also KT-7186 and forEachSafe.kt
// Custom `forEach` has no contract but the lambda is inline (not crossinline) so smart cast is safe
inline fun IntArray.forEachIndexed( op: (i: Int, value: Int) -> Unit) {
for (i in 0..this.size)
op(i, this[i])
}
fun max(a: IntArray): Int? {
var maxI: Int? = null
a.forEachIndexed { i, value ->
if (maxI == null || value >= a[maxI])
maxI = i
}
return maxI
}
@@ -1,7 +1,7 @@
// FIR_IDENTICAL // See also KT-7186 and forEachSafe.kt
// See also KT-7186 // Custom `forEach` has no contract but the lambda is inline (not crossinline) so smart cast is safe
fun IntArray.forEachIndexed( op: (i: Int, value: Int) -> Unit) { inline fun IntArray.forEachIndexed( op: (i: Int, value: Int) -> Unit) {
for (i in 0..this.size) for (i in 0..this.size)
op(i, this[i]) op(i, this[i])
} }
@@ -1,4 +1,4 @@
package package
public fun max(/*0*/ a: kotlin.IntArray): kotlin.Int? public fun max(/*0*/ a: kotlin.IntArray): kotlin.Int?
public fun kotlin.IntArray.forEachIndexed(/*0*/ op: (i: kotlin.Int, value: kotlin.Int) -> kotlin.Unit): kotlin.Unit public inline fun kotlin.IntArray.forEachIndexed(/*0*/ op: (i: kotlin.Int, value: kotlin.Int) -> kotlin.Unit): kotlin.Unit
@@ -0,0 +1,12 @@
// See also KT-7186 and varCapturedInInlineClosure.kt
// Standard library `forEach` calls lambda in-place by contract so smart cast is safe
fun indexOfMax(a: IntArray): Int? {
var maxI: Int? = null
a.forEachIndexed { i, value ->
if (maxI == null || value >= a[maxI]) {
maxI = i
}
}
return maxI
}
@@ -1,5 +1,5 @@
// FIR_IDENTICAL // See also KT-7186 and varCapturedInInlineClosure.kt
// KT-7186: False "Type mismatch" error // Standard library `forEach` calls lambda in-place by contract so smart cast is safe
fun indexOfMax(a: IntArray): Int? { fun indexOfMax(a: IntArray): Int? {
var maxI: Int? = null var maxI: Int? = null
@@ -30587,6 +30587,12 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest {
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt"); runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/iterations.kt");
} }
@Test
@TestMetadata("leakingLambdaInCalledInPlace.kt")
public void testLeakingLambdaInCalledInPlace() throws Exception {
runTest("compiler/testData/diagnostics/tests/smartCasts/varnotnull/leakingLambdaInCalledInPlace.kt");
}
@Test @Test
@TestMetadata("nestedDoWhile.kt") @TestMetadata("nestedDoWhile.kt")
public void testNestedDoWhile() throws Exception { public void testNestedDoWhile() throws Exception {