From 0ecc7528133cab7f0b970a0869f51b2076e5d7d5 Mon Sep 17 00:00:00 2001 From: Tianyu Geng Date: Thu, 13 May 2021 17:23:18 -0700 Subject: [PATCH] FIR checker: report SMARTCAST_IMPOSSIBLE for local variables --- ...irOldFrontendDiagnosticsTestGenerated.java | 18 + ...DiagnosticsWithLightTreeTestGenerated.java | 18 + .../kotlin/fir/resolve/ResolveUtils.kt | 12 +- .../fir/resolve/dfa/FirDataFlowAnalyzer.kt | 56 ++- .../dfa/FirLocalVariableAssignmentAnalyzer.kt | 359 ++++++++++++++++++ .../FirExpressionsResolveTransformer.kt | 3 + .../fir/declarations/FirDeclarationUtil.kt | 2 +- .../smartCasts/lambdaAndArgumentFun.fir.kt | 2 +- .../smartCasts/lambdaUsesOwnerModifies.fir.kt | 2 +- .../smartCasts/lambdaUsesOwnerModifies.kt | 2 +- .../tests/smartCasts/localClassChanges.fir.kt | 15 - .../tests/smartCasts/localClassChanges.kt | 1 + .../tests/smartCasts/localFunChanges.fir.kt | 14 - .../tests/smartCasts/localFunChanges.kt | 1 + .../smartCasts/localObjectChanges.fir.kt | 2 +- .../ownerDeclaresBothModifies.fir.kt | 4 +- .../smartCasts/ownerDeclaresBothModifies.kt | 2 +- .../variables/accessorAndFunction.fir.kt | 2 +- .../variables/capturedByAtLeastOnce.fir.kt | 56 +++ .../variables/capturedByAtLeastOnce.kt | 56 +++ .../variables/capturedByAtLeastOnce.txt | 12 + .../capturedByMultipleLambdas.fir.kt | 31 ++ .../variables/capturedByMultipleLambdas.kt | 31 ++ .../variables/capturedByMultipleLambdas.txt | 8 + .../variables/lambdaBetweenArguments.fir.kt | 2 +- .../capturedInClosureModifiedBefore.fir.kt | 10 +- .../varnotnull/varCapturedInClosure.fir.kt | 17 - .../varnotnull/varCapturedInClosure.kt | 3 +- .../varCapturedInInlineClosure.fir.kt | 15 - .../varnotnull/varCapturedInInlineClosure.kt | 1 + .../smartcasts/forEachSafe.fir.kt | 11 - .../testsWithStdLib/smartcasts/forEachSafe.kt | 1 + .../smartcasts/lambdaInCallArgs.fir.kt | 38 ++ .../smartcasts/lambdaInCallArgs.kt | 38 ++ .../smartcasts/lambdaInCallArgs.txt | 8 + .../test/runners/DiagnosticTestGenerated.java | 18 + .../diagnostics/notLinked/dfa/neg/18.fir.kt | 2 +- ...CompilerTestFE10TestdataTestGenerated.java | 18 + 38 files changed, 792 insertions(+), 99 deletions(-) create mode 100644 compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirLocalVariableAssignmentAnalyzer.kt delete mode 100644 compiler/testData/diagnostics/tests/smartCasts/localClassChanges.fir.kt delete mode 100644 compiler/testData/diagnostics/tests/smartCasts/localFunChanges.fir.kt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.fir.kt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.txt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.fir.kt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt create mode 100644 compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.txt delete mode 100644 compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.fir.kt delete mode 100644 compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.fir.kt delete mode 100644 compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.fir.kt create mode 100644 compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.fir.kt create mode 100644 compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt create mode 100644 compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.txt diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java index 87be5104a58..133cac855f3 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsTestGenerated.java @@ -27510,6 +27510,18 @@ public class FirOldFrontendDiagnosticsTestGenerated extends AbstractFirDiagnosti runTest("compiler/testData/diagnostics/tests/smartCasts/variables/assignmentConversion.kt"); } + @Test + @TestMetadata("capturedByAtLeastOnce.kt") + public void testCapturedByAtLeastOnce() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt"); + } + + @Test + @TestMetadata("capturedByMultipleLambdas.kt") + public void testCapturedByMultipleLambdas() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt"); + } + @Test @TestMetadata("doWhileWithMiddleBreak.kt") public void testDoWhileWithMiddleBreak() throws Exception { @@ -35721,6 +35733,12 @@ public class FirOldFrontendDiagnosticsTestGenerated extends AbstractFirDiagnosti runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/kt10463.kt"); } + @Test + @TestMetadata("lambdaInCallArgs.kt") + public void testLambdaInCallArgs() throws Exception { + runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt"); + } + @Test @TestMetadata("lazyDeclaresAndModifies.kt") public void testLazyDeclaresAndModifies() throws Exception { diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java index 4ecee2e84ad..8c5307a904b 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendDiagnosticsWithLightTreeTestGenerated.java @@ -27510,6 +27510,18 @@ public class FirOldFrontendDiagnosticsWithLightTreeTestGenerated extends Abstrac runTest("compiler/testData/diagnostics/tests/smartCasts/variables/assignmentConversion.kt"); } + @Test + @TestMetadata("capturedByAtLeastOnce.kt") + public void testCapturedByAtLeastOnce() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt"); + } + + @Test + @TestMetadata("capturedByMultipleLambdas.kt") + public void testCapturedByMultipleLambdas() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt"); + } + @Test @TestMetadata("doWhileWithMiddleBreak.kt") public void testDoWhileWithMiddleBreak() throws Exception { @@ -35721,6 +35733,12 @@ public class FirOldFrontendDiagnosticsWithLightTreeTestGenerated extends Abstrac runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/kt10463.kt"); } + @Test + @TestMetadata("lambdaInCallArgs.kt") + public void testLambdaInCallArgs() throws Exception { + runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt"); + } + @Test @TestMetadata("lazyDeclaresAndModifies.kt") public void testLazyDeclaresAndModifies() throws Exception { diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/ResolveUtils.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/ResolveUtils.kt index e2d990200b5..5a2115ab121 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/ResolveUtils.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/ResolveUtils.kt @@ -253,6 +253,12 @@ fun BodyResolveComponents.transformQualifiedAccessUsingSmartcastInfo( ): FirQualifiedAccessExpression { val (stability, typesFromSmartCast) = dataFlowAnalyzer.getTypeUsingSmartcastInfo(qualifiedAccessExpression) ?: return qualifiedAccessExpression + val smartcastStability = stability.impliedSmartcastStability + ?: if (dataFlowAnalyzer.isAccessToUnstableLocalVariable(qualifiedAccessExpression)) { + SmartcastStability.CAPTURED_VARIABLE + } else { + SmartcastStability.STABLE_VALUE + } val originalType = qualifiedAccessExpression.resultType.coneType // For example, if (x == null) { ... }, // we don't want to smartcast to Nothing?, but we want to record the nullability to its own kind of node. @@ -276,8 +282,7 @@ fun BodyResolveComponents.transformQualifiedAccessUsingSmartcastInfo( smartcastType = intersectedTypeRefWithoutNullableNothing // NB: Nothing? in types from smartcast in DFA is recorded here (and the expression kind itself). this.typesFromSmartCast = typesFromSmartCast - // TODO: differentiate capture local variable - this.smartcastStability = stability.impliedSmartcastStability ?: SmartcastStability.STABLE_VALUE + this.smartcastStability = smartcastStability } } val allTypes = typesFromSmartCast.also { @@ -295,8 +300,7 @@ fun BodyResolveComponents.transformQualifiedAccessUsingSmartcastInfo( originalExpression = qualifiedAccessExpression smartcastType = intersectedTypeRef this.typesFromSmartCast = typesFromSmartCast - // TODO: differentiate capture local variable - this.smartcastStability = stability.impliedSmartcastStability ?: SmartcastStability.STABLE_VALUE + this.smartcastStability = smartcastStability } } diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt index ab25fbd0eec..6976fb92c08 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirDataFlowAnalyzer.kt @@ -39,6 +39,7 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.name.StandardClassIds import org.jetbrains.kotlin.types.ConstantValueKind import org.jetbrains.kotlin.utils.addIfNotNull +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull class DataFlowAnalyzerContext( val graphBuilder: ControlFlowGraphBuilder, @@ -52,6 +53,8 @@ class DataFlowAnalyzerContext( var variableStorage = variableStorage private set + internal var firLocalVariableAssignmentAnalyzer: FirLocalVariableAssignmentAnalyzer? = null + private var assignmentCounter = 0 fun newAssignmentIndex(): Int { @@ -66,11 +69,12 @@ class DataFlowAnalyzerContext( flowOnNodes = mutableMapOf() preliminaryLoopVisitor.resetState() + firLocalVariableAssignmentAnalyzer = null } companion object { fun empty(session: FirSession): DataFlowAnalyzerContext = - DataFlowAnalyzerContext( + DataFlowAnalyzerContext( ControlFlowGraphBuilder(), VariableStorage(session), mutableMapOf(), mutableMapOf(), PreliminaryLoopVisitor() ) @@ -162,6 +166,10 @@ abstract class FirDataFlowAnalyzer( // ----------------------------------- Requests ----------------------------------- + fun isAccessToUnstableLocalVariable(qualifiedAccessExpression: FirQualifiedAccessExpression): Boolean { + return context.firLocalVariableAssignmentAnalyzer?.isAccessToUnstableLocalVariable(qualifiedAccessExpression) == true + } + fun getTypeUsingSmartcastInfo(qualifiedAccessExpression: FirQualifiedAccessExpression): Pair>? { /* * DataFlowAnalyzer holds variables only for declarations that have some smartcast (or can have) @@ -209,6 +217,9 @@ abstract class FirDataFlowAnalyzer( enterAnonymousFunction(function) return } + // All non-lambda function are treated as concurrent since we do not make any assumption about when and how it's invoked. + getOrCreateLocalVariableAssignmentAnalyzer(function)?.enterLocalFunction(function) + val (functionEnterNode, localFunctionNode, previousNode) = graphBuilder.enterFunction(function) localFunctionNode?.mergeIncomingFlow() functionEnterNode.mergeIncomingFlow(shouldForkFlow = previousNode != null) @@ -218,6 +229,9 @@ abstract class FirDataFlowAnalyzer( if (function is FirAnonymousFunction) { return exitAnonymousFunction(function) } + // All non-lambda function are treated as concurrent since we do not make any assumption about when and how it's invoked. + getOrCreateLocalVariableAssignmentAnalyzer(function)?.exitLocalFunction(function) + val (node, graph) = graphBuilder.exitFunction(function) node.mergeIncomingFlow() if (!graphBuilder.isTopLevel()) { @@ -237,6 +251,10 @@ abstract class FirDataFlowAnalyzer( // ----------------------------------- Anonymous function ----------------------------------- private fun enterAnonymousFunction(anonymousFunction: FirAnonymousFunction) { + getOrCreateLocalVariableAssignmentAnalyzer(anonymousFunction)?.apply { + finishPostponedAnonymousFunction() + enterLocalFunction(anonymousFunction) + } val (postponedLambdaEnterNode, functionEnterNode) = graphBuilder.enterAnonymousFunction(anonymousFunction) // TODO: questionable postponedLambdaEnterNode?.mergeIncomingFlow() @@ -244,18 +262,20 @@ abstract class FirDataFlowAnalyzer( } private fun exitAnonymousFunction(anonymousFunction: FirAnonymousFunction): FirControlFlowGraphReference { + getOrCreateLocalVariableAssignmentAnalyzer(anonymousFunction)?.exitLocalFunction( + anonymousFunction + ) val (functionExitNode, postponedLambdaExitNode, graph) = graphBuilder.exitAnonymousFunction(anonymousFunction) // TODO: questionable postponedLambdaExitNode?.mergeIncomingFlow() functionExitNode.mergeIncomingFlow() - exitCapturingStatement(anonymousFunction) return FirControlFlowGraphReferenceImpl(graph) } fun visitPostponedAnonymousFunction(anonymousFunction: FirAnonymousFunction) { + getOrCreateLocalVariableAssignmentAnalyzer(anonymousFunction)?.visitPostponedAnonymousFunction(anonymousFunction) val (enterNode, exitNode) = graphBuilder.visitPostponedAnonymousFunction(anonymousFunction) enterNode.mergeIncomingFlow() - enterCapturingStatement(enterNode, anonymousFunction) exitNode.mergeIncomingFlow() enterNode.flow = enterNode.flow.fork() } @@ -865,8 +885,19 @@ abstract class FirDataFlowAnalyzer( graphBuilder.enterCall() } + fun enterFunctionCall(functionCall: FirFunctionCall) { + val lambdaArgs = functionCall.arguments.filterIsInstance() + if (lambdaArgs.size > 1) { + getOrCreateLocalVariableAssignmentAnalyzer(lambdaArgs.first())?.enterFunctionCallWithMultipleLambdaArgs(lambdaArgs) + } + } + @OptIn(PrivateForInline::class) fun exitFunctionCall(functionCall: FirFunctionCall, callCompleted: Boolean) { + val lambdaArgs = functionCall.arguments.filterIsInstance() + if (lambdaArgs.size > 1) { + getOrCreateLocalVariableAssignmentAnalyzer(lambdaArgs.first())?.enterFunctionCallWithMultipleLambdaArgs(lambdaArgs) + } if (ignoreFunctionCalls) { graphBuilder.exitIgnoredCall(functionCall) return @@ -1025,9 +1056,12 @@ abstract class FirDataFlowAnalyzer( variableStorage.getOrCreateRealVariable(flow, initializer.symbol, initializer) ?.let { initializerVariable -> - // TODO: handle capture variable - if ((initializerVariable.stability == PropertyStability.STABLE_VALUE || initializerVariable.stability == PropertyStability.LOCAL_VAR) && - (propertyVariable.stability == PropertyStability.STABLE_VALUE || propertyVariable.stability == PropertyStability.LOCAL_VAR) + val isInitializerStable = initializerVariable.stability == PropertyStability.STABLE_VALUE || + (initializerVariable.stability == PropertyStability.LOCAL_VAR && + initializer is FirQualifiedAccessExpression && + !isAccessToUnstableLocalVariable(initializer)) + + if (isInitializerStable && (propertyVariable.stability == PropertyStability.STABLE_VALUE || propertyVariable.stability == PropertyStability.LOCAL_VAR) ) { logicSystem.addLocalVariableAlias( flow, propertyVariable, @@ -1293,6 +1327,16 @@ abstract class FirDataFlowAnalyzer( // ------------------------------------------------------ Utils ------------------------------------------------------ + private fun getOrCreateLocalVariableAssignmentAnalyzer(firFunction: FirFunction<*>): FirLocalVariableAssignmentAnalyzer? { + // Only return analyzer for nested functions so that we won't waste time on functions that don't contain any lambda or local + // function. + val rootFunction = components.containingDeclarations.firstIsInstanceOrNull>() ?: return null + if (rootFunction == firFunction) return null + return context.firLocalVariableAssignmentAnalyzer ?: FirLocalVariableAssignmentAnalyzer.analyzeFunction(rootFunction).also { + context.firLocalVariableAssignmentAnalyzer = it + } + } + private var CFGNode<*>.flow: FLOW get() = context.flowOnNodes.getValue(this.origin) set(value) { diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirLocalVariableAssignmentAnalyzer.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirLocalVariableAssignmentAnalyzer.kt new file mode 100644 index 00000000000..b54ac8cd69e --- /dev/null +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/dfa/FirLocalVariableAssignmentAnalyzer.kt @@ -0,0 +1,359 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.fir.resolve.dfa + +import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange +import org.jetbrains.kotlin.fir.FirElement +import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.expressions.* +import org.jetbrains.kotlin.fir.references.FirNamedReference +import org.jetbrains.kotlin.fir.resolve.dfa.FirLocalVariableAssignmentAnalyzer.Companion.MiniFlow.Companion.join +import org.jetbrains.kotlin.fir.visitors.FirVisitor +import org.jetbrains.kotlin.name.Name + +/** + * Helper that checks if an access to a local variable access is stable. + * + * To determine the stability of an access, call [isAccessToUnstableLocalVariable]. Note that the class contains mutable states. So + * [isAccessToUnstableLocalVariable] only works for an access during the natural FIR tree traversal. This class will not work if one + * queries after the traversal is done. + **/ +internal class FirLocalVariableAssignmentAnalyzer( + private val assignedLocalVariablesByFunction: Map, AssignedLocalVariables> +) { + /** + * Stack storing concurrent lambda arguments for the current visited anonymous function. For example + * ``` + * callWithMultipleLambdaExactlyOnceEach( + * l1 = { x.length }, + * l2 = { x = null } + * ) + * ``` + * From the call, it's nondeterministic whether `l1` runs before `l2` or vice versa. So when handling `l1`, we must mark all variables + * touched in `l2` unstable. + */ + private val concurrentLambdaArgsStack: MutableList> = mutableListOf() + + /** + * Stack whose element tracks all concurrently modified variables in execution paths other than this one. It's a stack because after + * exiting a local function, we must restore the variables to the state before entering this local function. Initially, there is an + * empty set for the root function when we starts the analysis. + */ + private val concurrentlyAssignedLocalVariablesStack: MutableList> = mutableListOf(mutableSetOf()) + + /** + * Temporary storage that tracks concurrently modified variables during function call resolution. For example, consider the following, + * + * ``` + * foo(bar { x= null }, x.length) + * ``` + * + * Sometimes during resolution, when stability of `x` is retrieved with [isAccessToUnstableLocalVariable], the resolution of `foo` and + * `bar` is not yet finished. Hence, the lambda arg passed to `bar` is not traversed. In this case, the resolution logic first calls + * [visitPostponedAnonymousFunction], then [isAccessToUnstableLocalVariable]. Next, it calls [enterLocalFunction] and starts traversing + * the lambda passed to `bar`. + */ + private val ephemeralConcurrentlyAssignedLocalVariables: MutableSet = mutableSetOf() + + /** Checks whether the given access is an unstable access to a local variable at this moment. */ + fun isAccessToUnstableLocalVariable(qualifiedAccessExpression: FirQualifiedAccessExpression): Boolean { + val property = qualifiedAccessExpression.referredPropertySymbol?.fir ?: return false + return property in ephemeralConcurrentlyAssignedLocalVariables || property in concurrentlyAssignedLocalVariablesStack.last() + } + + fun visitPostponedAnonymousFunction(anonymousFunction: FirAnonymousFunction) { + // Postponed anonymous function is visited before the current function call with lambda is resolved. Hence, the invocationKind is + // 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, + // `exitAnonymousFunction` will be invoked at some point to properly set up the `persistentConcurrentlyAssignedLocalVariables`. + assignedLocalVariablesByFunction[anonymousFunction]?.insideLocalFunction?.let { + ephemeralConcurrentlyAssignedLocalVariables.addAll(it) + } + } + + fun finishPostponedAnonymousFunction() { + // Clear the temporarily assigned local variables in `visitPostponedAnonymousFunction`. + ephemeralConcurrentlyAssignedLocalVariables.clear() + } + + fun enterLocalFunction(function: FirFunction<*>) { + val eventOccurrencesRange: EventOccurrencesRange? = (function as? FirAnonymousFunction)?.invocationKind + // carry on concurrently modified variables from the current scope + val concurrentlyAssignedLocalVariables = concurrentlyAssignedLocalVariablesStack.last().toMutableSet() + concurrentlyAssignedLocalVariablesStack.add(concurrentlyAssignedLocalVariables) + val concurrentLambdasInCurrentCall = concurrentLambdaArgsStack.lastOrNull() + if (concurrentLambdasInCurrentCall != null && function in concurrentLambdasInCurrentCall) { + concurrentLambdasInCurrentCall.filter { it != function }.forEach { otherLambda -> + assignedLocalVariablesByFunction[otherLambda]?.insideLocalFunction?.let { + concurrentlyAssignedLocalVariables += it + } + } + } + when (eventOccurrencesRange) { + EventOccurrencesRange.AT_LEAST_ONCE, + EventOccurrencesRange.MORE_THAN_ONCE -> assignedLocalVariablesByFunction[function]?.insideLocalFunction?.let { + concurrentlyAssignedLocalVariables += it + } + // Add both inside and outside since this local function may be invoked multiple times concurrently. + EventOccurrencesRange.UNKNOWN, null -> assignedLocalVariablesByFunction[function]?.all?.let { + concurrentlyAssignedLocalVariables += it + } + else -> { + // no additional stuff to do for other cases + } + } + } + + fun exitLocalFunction(function: FirFunction<*>) { + val eventOccurrencesRange: EventOccurrencesRange? = (function as? FirAnonymousFunction)?.invocationKind + concurrentlyAssignedLocalVariablesStack.removeLast() + when (eventOccurrencesRange) { + EventOccurrencesRange.UNKNOWN, null -> assignedLocalVariablesByFunction[function]?.insideLocalFunction?.let { + concurrentlyAssignedLocalVariablesStack.last() += it + } + else -> { + // no additional stuff to do for other cases + } + } + } + + fun enterFunctionCallWithMultipleLambdaArgs(lambdaArgs: List) { + concurrentLambdaArgsStack.add(lambdaArgs.toSet()) + } + + fun exitFunctionCallWithMultipleLambdaArgs() { + concurrentLambdaArgsStack.removeLast() + } + + companion object { + /** + * Computes assigned local variables in each execution path. This analyzer runs before BODY_RESOLVE. Hence, it works on + * syntactical information only. + * + * # Note on implementation detail + * + * The analyzer constructs a mini control flow graph that captures forking of execution path. Any conditional branches, declaration of + * lambda and local functions are forks. The mini CFG does not care about loop structures and effectively treats it as a linear sequence of + * statements. This is sufficient for the purpose of collecting unstable local variables. Similarly for try/catch/finally constructs. + * + * 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. + * + * ``` + * fun test() { + * var x: Int = 0 + * var y: Int = 0 + * var z: Int = 0 + * if (true) { + * run { + * x = 1 + * var a = 1 + * a = 2 + * } + * } else { + * x = 2 + * y = 2 + * } + * z = 3 + * } + * ``` + * + * The generated mini CFG looks like the following, with assigned local variables annotated after each node in curly brackets. + * + * ┌───────┐ + * │ if │ {x y z a} + * └─┬─┬─┬─┘ + * │ │ │ fallback + * │ │ └──────────────────────────────────────┐ + * │ │ false │ + * │ └─────────────────────────┐ │ + * │ true │ │ + * ┌─┴─────┐ ┌───┴────┐ │ + * │ run │ {x z a} │ else │ {x y z} │ + * │ │ │ branch │ │ + * └─┬───┬─┘ └───┬────┘ │ + * │ │ normal execution │ │ + * │ └─────────────┐ │ │ + * │ lambda arg │ │ │ + * ┌─┴──────┐ ┌───┴───┐ │ │ + * │ lambda │ {x} │ empty │ {z} │ │ + * │ body │ │ │ │ │ + * └────────┘ └───┬───┘ │ │ + * ┌─────────────────┘ │ │ + * │ ┌─────────────────────────┘ │ + * │ │ ┌──────────────────────────────────────┘ + * ┌─┴─┴─┴─┐ + * │ after │ {z} + * │ if │ + * └───────┘ + * + * Some notes on why each node contains what it contains: + * + * - 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 + * [MiniCfgBuilder.handleFunctionFork] after the lambda function is processed. 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 + * referenced outside the lambda. It's possible to track the scope at each node and remove the unneeded `a` in "if" and "run" + * 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. + * + * By the way, since local variables 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. + */ + fun analyzeFunction(rootFunction: FirFunction<*>): FirLocalVariableAssignmentAnalyzer { + return FirLocalVariableAssignmentAnalyzer(computeAssignedLocalVariables(rootFunction)) + } + + /** + * Computes a mini CFG and returns the map tracking assigned local variables at each potentially concurrent local/lambda function. + */ + private fun computeAssignedLocalVariables(firFunction: FirFunction<*>): Map, AssignedLocalVariables> { + val startFlow = MiniFlow.start() + val data = MiniCfgBuilder.MiniCfgData(startFlow) + MiniCfgBuilder().visitElement(firFunction, data) + return data.localFunctionToAssignedLocalVariables + } + + class AssignedLocalVariables(val outsideLocalFunction: Set, val insideLocalFunction: Set) { + val all get() = outsideLocalFunction + insideLocalFunction + } + + private class MiniFlow(val parents: Set) { + val assignedLocalVariables: MutableSet = mutableSetOf() + + fun fork(): MiniFlow = MiniFlow(setOf(this)) + + companion object { + fun start() = MiniFlow(emptySet()) + fun Set.join(): MiniFlow = MiniFlow(this) + } + } + + + private class MiniCfgBuilder : FirVisitor() { + override fun visitElement(element: FirElement, data: MiniCfgData) { + element.acceptChildren(this, data) + } + + override fun visitAnonymousFunction(anonymousFunction: FirAnonymousFunction, data: MiniCfgData) { + handleFunctionFork(anonymousFunction, data) + } + + override fun visitSimpleFunction(simpleFunction: FirSimpleFunction, data: MiniCfgData) { + handleFunctionFork(simpleFunction, data) + } + + private fun handleFunctionFork(function: FirFunction<*>, data: MiniCfgData) { + val currentFlow = data.flow ?: return + val functionFork = currentFlow.fork() + data.flow = functionFork + function.acceptChildren(this, data) + + // 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 + // just work because inside the lambda assigned local variables are tracked by different nodes). + functionFork.assignedLocalVariables.retainAll(data.variableDeclarations.flatMap { it.values }) + // Create another fork for the normal execution + val normalExecution = currentFlow.fork() + data.localFunctionToAssignedLocalVariables[function] = + AssignedLocalVariables(normalExecution.assignedLocalVariables, functionFork.assignedLocalVariables) + data.flow = normalExecution + } + + override fun visitWhenExpression(whenExpression: FirWhenExpression, data: MiniCfgData) { + val flow = data.flow ?: return + val visitor = this + with(whenExpression) { + calleeReference.accept(visitor, data) + val subjectVariable = this.subjectVariable + if (subjectVariable != null) { + subjectVariable.accept(visitor, data) + } else { + subject?.accept(visitor, data) + } + val childFlows = branches.mapNotNull { + data.flow = flow.fork() + 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() + } + } + + override fun visitReturnExpression(returnExpression: FirReturnExpression, data: MiniCfgData) { + // TODO: consider to also handle `throw`, which would require keeping track of all `try`, `catch` and `finally` constructs. + data.flow = null + } + + @OptIn(ExperimentalStdlibApi::class) + override fun visitFunctionCall(functionCall: FirFunctionCall, data: MiniCfgData) { + val visitor = this + with(functionCall) { + setOfNotNull(explicitReceiver, dispatchReceiver, extensionReceiver).forEach { it.accept(visitor, data) } + // Delay processing of lambda args because lambda body are evaluated after all arguments have been evaluated. + val (postponedFunctionArgs, normalArgs) = argumentList.arguments.partition { it is FirAnonymousFunction } + normalArgs.forEach { it.accept(visitor, data) } + postponedFunctionArgs.forEach { it.accept(visitor, data) } + calleeReference.accept(visitor, data) + } + } + + override fun visitBlock(block: FirBlock, data: MiniCfgData) { + data.variableDeclarations.addLast(mutableMapOf()) + super.visitBlock(block, data) + data.variableDeclarations.removeLast() + } + + override fun visitProperty(property: FirProperty, data: MiniCfgData) { + if (property.isLocal) { + data.variableDeclarations.last()[property.name] = property + } + } + + override fun visitVariableAssignment(variableAssignment: FirVariableAssignment, data: MiniCfgData) { + val flow = data.flow ?: return + val name = (variableAssignment.lValue as? FirNamedReference)?.name ?: return + flow.recordAssignment(name, data) + } + + override fun visitAssignmentOperatorStatement(assignmentOperatorStatement: FirAssignmentOperatorStatement, data: MiniCfgData) { + val flow = data.flow ?: return + val lhs = assignmentOperatorStatement.leftArgument as? FirQualifiedAccessExpression ?: return + if (lhs.explicitReceiver != null) return + val name = (lhs.calleeReference as? FirNamedReference)?.name ?: return + flow.recordAssignment(name, data) + } + + fun MiniFlow.recordAssignment(name: Name, data: MiniCfgData) { + val property = data.resolveLocalVariable(name) ?: return + recordAssignment(property, mutableSetOf()) + } + + private fun MiniFlow.recordAssignment(property: FirProperty, visited: MutableSet) { + if (this in visited) return + visited += this + assignedLocalVariables += property + // Back-propagate the assignment to all parent flows. + parents.forEach { it.recordAssignment(property, visited) } + } + + class MiniCfgData(var flow: MiniFlow?) { + val variableDeclarations: ArrayDeque> = ArrayDeque(listOf(mutableMapOf())) + val localFunctionToAssignedLocalVariables: MutableMap, AssignedLocalVariables> = mutableMapOf() + fun resolveLocalVariable(name: Name): FirProperty? { + return variableDeclarations.asReversed().firstNotNullOfOrNull { it[name] } + } + } + } + } +} \ No newline at end of file diff --git a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/FirExpressionsResolveTransformer.kt b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/FirExpressionsResolveTransformer.kt index 7c40ae080b9..e3e0d94a202 100644 --- a/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/FirExpressionsResolveTransformer.kt +++ b/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/body/resolve/FirExpressionsResolveTransformer.kt @@ -283,6 +283,7 @@ open class FirExpressionsResolveTransformer(transformer: FirBodyResolveTransform functionCall.transformAnnotations(transformer, data) functionCall.transformSingle(InvocationKindTransformer, null) functionCall.transformTypeArguments(transformer, ResolutionMode.ContextIndependent) + dataFlowAnalyzer.enterFunctionCall(functionCall) val (completeInference, callCompleted) = try { val initialExplicitReceiver = functionCall.explicitReceiver @@ -421,12 +422,14 @@ open class FirExpressionsResolveTransformer(transformer: FirBodyResolveTransform val lhsIsVar = lhsVariable?.isVar == true fun chooseAssign(): FirStatement { + dataFlowAnalyzer.enterFunctionCall(resolvedAssignCall) callCompleter.completeCall(resolvedAssignCall, noExpectedType) dataFlowAnalyzer.exitFunctionCall(resolvedAssignCall, callCompleted = true) return resolvedAssignCall } fun chooseOperator(): FirStatement { + dataFlowAnalyzer.enterFunctionCall(resolvedAssignCall) callCompleter.completeCall( resolvedOperatorCall, lhsVariable?.returnTypeRef ?: noExpectedType, diff --git a/compiler/fir/tree/src/org/jetbrains/kotlin/fir/declarations/FirDeclarationUtil.kt b/compiler/fir/tree/src/org/jetbrains/kotlin/fir/declarations/FirDeclarationUtil.kt index f5b514f2b21..3fae2fd4efa 100644 --- a/compiler/fir/tree/src/org/jetbrains/kotlin/fir/declarations/FirDeclarationUtil.kt +++ b/compiler/fir/tree/src/org/jetbrains/kotlin/fir/declarations/FirDeclarationUtil.kt @@ -101,7 +101,7 @@ inline val FirPropertyAccessor.allowsToHaveFakeOverride: Boolean get() = !Visibilities.isPrivate(visibility) && visibility != Visibilities.InvisibleFake inline val FirClassLikeDeclaration<*>.isLocal get() = symbol.classId.isLocal -inline val FirSimpleFunction.isLocal get() = status.visibility == Visibilities.Local +inline val FirCallableMemberDeclaration<*>.isLocal get() = status.visibility == Visibilities.Local fun FirRegularClassBuilder.addDeclaration(declaration: FirDeclaration) { declarations += declaration diff --git a/compiler/testData/diagnostics/tests/smartCasts/lambdaAndArgumentFun.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/lambdaAndArgumentFun.fir.kt index 49b404f34d6..7e2fe4f2b01 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/lambdaAndArgumentFun.fir.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/lambdaAndArgumentFun.fir.kt @@ -10,5 +10,5 @@ fun use() { // Write to x is AFTER x.hashCode() // No smart cast should be here! - foo(bar { x = null }, x.hashCode()) + foo(bar { x = null }, x.hashCode()) } diff --git a/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.fir.kt index e33c1f9c5f3..16995f3ee40 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.fir.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.fir.kt @@ -3,7 +3,7 @@ fun foo(arg: Int?) { var x = arg if (x == null) return run { - // Not safe: x = null later in the owner + // Safe: since `run` is in-place x.hashCode() } x = null diff --git a/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.kt b/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.kt index 0889a3aaa16..8a54fdc86e6 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/lambdaUsesOwnerModifies.kt @@ -3,7 +3,7 @@ fun foo(arg: Int?) { var x = arg if (x == null) return run { - // Not safe: x = null later in the owner + // Safe: since `run` is in-place x.hashCode() } x = null diff --git a/compiler/testData/diagnostics/tests/smartCasts/localClassChanges.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/localClassChanges.fir.kt deleted file mode 100644 index badc7eea0b4..00000000000 --- a/compiler/testData/diagnostics/tests/smartCasts/localClassChanges.fir.kt +++ /dev/null @@ -1,15 +0,0 @@ -fun trans(n: Int, f: () -> Boolean) = if (f()) n else null - -fun foo() { - var i: Int? = 5 - if (i != null) { - class Changing { - fun bar() { - i = null - } - } - i.hashCode() - Changing().bar() - i.hashCode() - } -} diff --git a/compiler/testData/diagnostics/tests/smartCasts/localClassChanges.kt b/compiler/testData/diagnostics/tests/smartCasts/localClassChanges.kt index de9051f9ea1..179154bac2d 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/localClassChanges.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/localClassChanges.kt @@ -1,3 +1,4 @@ +// FIR_IDENTICAL fun trans(n: Int, f: () -> Boolean) = if (f()) n else null fun foo() { diff --git a/compiler/testData/diagnostics/tests/smartCasts/localFunChanges.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/localFunChanges.fir.kt deleted file mode 100644 index a844c9e3207..00000000000 --- a/compiler/testData/diagnostics/tests/smartCasts/localFunChanges.fir.kt +++ /dev/null @@ -1,14 +0,0 @@ -fun trans(n: Int, f: () -> Boolean) = if (f()) n else null - -fun foo() { - var i: Int? = 5 - if (i != null) { - fun can(): Boolean { - i = null - return true - } - i.hashCode() - trans(i, ::can) - i.hashCode() - } -} diff --git a/compiler/testData/diagnostics/tests/smartCasts/localFunChanges.kt b/compiler/testData/diagnostics/tests/smartCasts/localFunChanges.kt index 207c3d3d93f..dad48b4b109 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/localFunChanges.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/localFunChanges.kt @@ -1,3 +1,4 @@ +// FIR_IDENTICAL fun trans(n: Int, f: () -> Boolean) = if (f()) n else null fun foo() { diff --git a/compiler/testData/diagnostics/tests/smartCasts/localObjectChanges.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/localObjectChanges.fir.kt index fdcd38ed392..24f1c818504 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/localObjectChanges.fir.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/localObjectChanges.fir.kt @@ -10,6 +10,6 @@ fun foo() { i = null } }.bar() - i.hashCode() + i.hashCode() } } diff --git a/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.fir.kt index f9f45f3e355..7dff0d653f8 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.fir.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.fir.kt @@ -3,8 +3,8 @@ fun foo(arg: Int?) { var x = arg if (x == null) return run { - // Unsafe because of owner modification - x.hashCode() + // Stable because `run` is in-place + x.hashCode() x = null } if (x != null) x = 42 diff --git a/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.kt b/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.kt index 7d63e53c8a6..19bbc70e50c 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/ownerDeclaresBothModifies.kt @@ -3,7 +3,7 @@ fun foo(arg: Int?) { var x = arg if (x == null) return run { - // Unsafe because of owner modification + // Stable because `run` is in-place x.hashCode() x = null } diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/accessorAndFunction.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/accessorAndFunction.fir.kt index 8d4e295e1ea..501d430f48f 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/variables/accessorAndFunction.fir.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/accessorAndFunction.fir.kt @@ -1,6 +1,6 @@ class My { - val y: Int + val y: Int get() { var x: Int? x = 3 diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.fir.kt new file mode 100644 index 00000000000..91dfeece9fd --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.fir.kt @@ -0,0 +1,56 @@ +import kotlin.contracts.* + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +inline fun atLeastOnce(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.AT_LEAST_ONCE) + } + block() +} + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +inline fun atMostOnce(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + block() +} + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +inline fun exactlyOnce(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + block() +} + +fun test() { + var s: String? = null + s = "" + atLeastOnce { + s.length // unstable since lambda can be called twice + s = null + var s2: String? = null + s2 = "" + s2.length // local variable declared inside lambda is stable + s2 = null + } + s = "" + exactlyOnce { + s.length // stable since lambda can be called only once + s = null + var s2: String? = null + s2 = "" + s2.length // local variable declared inside lambda is stable + s2 = null + } + s = "" + atMostOnce { + s.length // stable since lambda can be called at most once + s = null + var s2: String? = null + s2 = "" + s2.length // local variable declared inside lambda is stable + s2 = null + } +} diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt new file mode 100644 index 00000000000..fcd45a203a7 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt @@ -0,0 +1,56 @@ +import kotlin.contracts.* + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +inline fun atLeastOnce(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.AT_LEAST_ONCE) + } + block() +} + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +inline fun atMostOnce(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + block() +} + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +inline fun exactlyOnce(block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + block() +} + +fun test() { + var s: String? = null + s = "" + atLeastOnce { + s.length // unstable since lambda can be called twice + s = null + var s2: String? = null + s2 = "" + s2.length // local variable declared inside lambda is stable + s2 = null + } + s = "" + exactlyOnce { + s.length // stable since lambda can be called only once + s = null + var s2: String? = null + s2 = "" + s2.length // local variable declared inside lambda is stable + s2 = null + } + s = "" + atMostOnce { + s.length // stable since lambda can be called at most once + s = null + var s2: String? = null + s2 = "" + s2.length // local variable declared inside lambda is stable + s2 = null + } +} diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.txt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.txt new file mode 100644 index 00000000000..2f2d89feb94 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.txt @@ -0,0 +1,12 @@ +package + +@kotlin.Suppress(names = {"EXPERIMENTAL_API_USAGE_ERROR"}) public inline fun atLeastOnce(/*0*/ block: () -> kotlin.Unit): kotlin.Unit + CallsInPlace(block, AT_LEAST_ONCE) + +@kotlin.Suppress(names = {"EXPERIMENTAL_API_USAGE_ERROR"}) public inline fun atMostOnce(/*0*/ block: () -> kotlin.Unit): kotlin.Unit + CallsInPlace(block, AT_MOST_ONCE) + +@kotlin.Suppress(names = {"EXPERIMENTAL_API_USAGE_ERROR"}) public inline fun exactlyOnce(/*0*/ block: () -> kotlin.Unit): kotlin.Unit + CallsInPlace(block, EXACTLY_ONCE) + +public fun test(): kotlin.Unit diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.fir.kt new file mode 100644 index 00000000000..e5a352a9525 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.fir.kt @@ -0,0 +1,31 @@ +import kotlin.contracts.* + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +fun foo(f1: () -> Unit, f2: () -> Unit) { + contract { + callsInPlace(f1, InvocationKind.EXACTLY_ONCE) + callsInPlace(f2, InvocationKind.EXACTLY_ONCE) + } + f2() + f1() +} + +fun test() { + var s: String? = null + s = "" + foo( + { s.length }, // unstable since lambda evaluation order is indeterministic + { s = null }, + ) + s = "" + foo( + { s = null }, + { s.length }, // unstable since lambda evaluation order is indeterministic + ) + s = "" + foo( + { s.length }, // stable + { s.length }, // stable + ) + s = null +} diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt new file mode 100644 index 00000000000..6024e25e3bd --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt @@ -0,0 +1,31 @@ +import kotlin.contracts.* + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR") +fun foo(f1: () -> Unit, f2: () -> Unit) { + contract { + callsInPlace(f1, InvocationKind.EXACTLY_ONCE) + callsInPlace(f2, InvocationKind.EXACTLY_ONCE) + } + f2() + f1() +} + +fun test() { + var s: String? = null + s = "" + foo( + { s.length }, // unstable since lambda evaluation order is indeterministic + { s = null }, + ) + s = "" + foo( + { s = null }, + { s.length }, // unstable since lambda evaluation order is indeterministic + ) + s = "" + foo( + { s.length }, // stable + { s.length }, // stable + ) + s = null +} diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.txt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.txt new file mode 100644 index 00000000000..c81dccc5b73 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.txt @@ -0,0 +1,8 @@ +package + +@kotlin.Suppress(names = {"EXPERIMENTAL_API_USAGE_ERROR"}) public fun foo(/*0*/ f1: () -> kotlin.Unit, /*1*/ f2: () -> kotlin.Unit): kotlin.Unit + CallsInPlace(f1, EXACTLY_ONCE) + CallsInPlace(f2, EXACTLY_ONCE) + +public fun test(): kotlin.Unit + diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/lambdaBetweenArguments.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/lambdaBetweenArguments.fir.kt index da5b098c670..33d048b4a54 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/variables/lambdaBetweenArguments.fir.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/variables/lambdaBetweenArguments.fir.kt @@ -5,5 +5,5 @@ fun foo(x: Int, f: () -> Unit, y: Int) {} fun bar() { var x: Int? x = 4 - foo(x, { x = null; x.hashCode() }, x) + foo(x, { x = null; x.hashCode() }, x) } diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.fir.kt index 3998cb0d6ed..4475e956293 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.fir.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/capturedInClosureModifiedBefore.fir.kt @@ -27,10 +27,10 @@ fun baz(s: String?) { var x = s if (x != null) { run { - x.hashCode() + x.hashCode() } run { - x.hashCode() + x.hashCode() x = null } } @@ -40,11 +40,11 @@ fun gaz(s: String?) { var x = s if (x != null) { run { - x.hashCode() + x.hashCode() x = null } run { - x.hashCode() + x.hashCode() } } } @@ -53,7 +53,7 @@ fun gav(s: String?) { var x = s if (x != null) { run { - x.hashCode() + x.hashCode() } x = null } diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.fir.kt deleted file mode 100644 index ad11d03afd1..00000000000 --- a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.fir.kt +++ /dev/null @@ -1,17 +0,0 @@ -public fun foo() { - var s: String? = "" - fun closure(): Int { - if (s == "") { - s = null - return -1 - } else if (s == null) { - return -2 - } else { - return s.length // Here smartcast is possible, at least in principle - } - } - if (s != null) { - System.out.println(closure()) - System.out.println(s.length) // Here smartcast is not possible due to a closure predecessor - } -} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.kt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.kt index cd36cd908b5..6db096c825f 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInClosure.kt @@ -1,3 +1,4 @@ +// FIR_IDENTICAL public fun foo() { var s: String? = "" fun closure(): Int { @@ -14,4 +15,4 @@ public fun foo() { System.out.println(closure()) System.out.println(s.length) // Here smartcast is not possible due to a closure predecessor } -} \ No newline at end of file +} diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.fir.kt deleted file mode 100644 index 2eeb814a921..00000000000 --- a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.fir.kt +++ /dev/null @@ -1,15 +0,0 @@ -// See also KT-7186 - -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 -} diff --git a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.kt b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.kt index c8d713bc51c..35d671353ef 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/varnotnull/varCapturedInInlineClosure.kt @@ -1,3 +1,4 @@ +// FIR_IDENTICAL // See also KT-7186 fun IntArray.forEachIndexed( op: (i: Int, value: Int) -> Unit) { diff --git a/compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.fir.kt b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.fir.kt deleted file mode 100644 index 76650fde7d0..00000000000 --- a/compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.fir.kt +++ /dev/null @@ -1,11 +0,0 @@ -// KT-7186: False "Type mismatch" error - -fun indexOfMax(a: IntArray): Int? { - var maxI: Int? = null - a.forEachIndexed { i, value -> - if (maxI == null || value >= a[maxI]) { - maxI = i - } - } - return maxI -} diff --git a/compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.kt b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.kt index 6457395997b..1789f174cf2 100644 --- a/compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.kt +++ b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/forEachSafe.kt @@ -1,3 +1,4 @@ +// FIR_IDENTICAL // KT-7186: False "Type mismatch" error fun indexOfMax(a: IntArray): Int? { diff --git a/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.fir.kt b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.fir.kt new file mode 100644 index 00000000000..b9aaea8a1c1 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.fir.kt @@ -0,0 +1,38 @@ +fun foo(x: Int, y: Any?, z: Int) {} + +fun myRun(block: () -> Unit): Any? { + return null +} + +fun test_1() { + var x: String? = null + if (x != null) { + foo( + x.length, // stable smartcast + run { x = "" }, + x.length // stable smartcast + ) + } +} + +fun test_2() { + var x: String? = null + if (x != null) { + foo( + x.length, // stable smartcast + myRun { x = "" }, + x.length // unstable smartcast + ) + } +} + +fun test_3() { + var x: String? = null + if (x != null) { + foo( + x.length, // stable smartcast + { x = "" }, + x.length // stable smartcast + ) + } +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt new file mode 100644 index 00000000000..afb27c65940 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt @@ -0,0 +1,38 @@ +fun foo(x: Int, y: Any?, z: Int) {} + +fun myRun(block: () -> Unit): Any? { + return null +} + +fun test_1() { + var x: String? = null + if (x != null) { + foo( + x.length, // stable smartcast + run { x = "" }, + x.length // stable smartcast + ) + } +} + +fun test_2() { + var x: String? = null + if (x != null) { + foo( + x.length, // stable smartcast + myRun { x = "" }, + x.length // unstable smartcast + ) + } +} + +fun test_3() { + var x: String? = null + if (x != null) { + foo( + x.length, // stable smartcast + { x = "" }, + x.length // stable smartcast + ) + } +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.txt b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.txt new file mode 100644 index 00000000000..4a8237b2eff --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.txt @@ -0,0 +1,8 @@ +package + +public fun foo(/*0*/ x: kotlin.Int, /*1*/ y: kotlin.Any?, /*2*/ z: kotlin.Int): kotlin.Unit +public fun myRun(/*0*/ block: () -> kotlin.Unit): kotlin.Any? +public fun test_1(): kotlin.Unit +public fun test_2(): kotlin.Unit +public fun test_3(): kotlin.Unit + diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java index c022e33e833..5739530b18d 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java @@ -27600,6 +27600,18 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest { runTest("compiler/testData/diagnostics/tests/smartCasts/variables/assignmentConversion.kt"); } + @Test + @TestMetadata("capturedByAtLeastOnce.kt") + public void testCapturedByAtLeastOnce() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt"); + } + + @Test + @TestMetadata("capturedByMultipleLambdas.kt") + public void testCapturedByMultipleLambdas() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt"); + } + @Test @TestMetadata("doWhileWithMiddleBreak.kt") public void testDoWhileWithMiddleBreak() throws Exception { @@ -35817,6 +35829,12 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest { runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/kt10463.kt"); } + @Test + @TestMetadata("lambdaInCallArgs.kt") + public void testLambdaInCallArgs() throws Exception { + runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt"); + } + @Test @TestMetadata("lazyDeclaresAndModifies.kt") public void testLazyDeclaresAndModifies() throws Exception { diff --git a/compiler/tests-spec/testData/diagnostics/notLinked/dfa/neg/18.fir.kt b/compiler/tests-spec/testData/diagnostics/notLinked/dfa/neg/18.fir.kt index c3de82369bd..f559352d7b4 100644 --- a/compiler/tests-spec/testData/diagnostics/notLinked/dfa/neg/18.fir.kt +++ b/compiler/tests-spec/testData/diagnostics/notLinked/dfa/neg/18.fir.kt @@ -38,7 +38,7 @@ fun case_3(x: Interface1?) { var y = x y as Interface2 fun foo() { - y.itest2() + y.itest2() } y = null foo() diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java index 283027ce8ad..bbe98f04a11 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/diagnostic/compiler/based/DiagnosisCompilerTestFE10TestdataTestGenerated.java @@ -27510,6 +27510,18 @@ public class DiagnosisCompilerTestFE10TestdataTestGenerated extends AbstractDiag runTest("compiler/testData/diagnostics/tests/smartCasts/variables/assignmentConversion.kt"); } + @Test + @TestMetadata("capturedByAtLeastOnce.kt") + public void testCapturedByAtLeastOnce() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByAtLeastOnce.kt"); + } + + @Test + @TestMetadata("capturedByMultipleLambdas.kt") + public void testCapturedByMultipleLambdas() throws Exception { + runTest("compiler/testData/diagnostics/tests/smartCasts/variables/capturedByMultipleLambdas.kt"); + } + @Test @TestMetadata("doWhileWithMiddleBreak.kt") public void testDoWhileWithMiddleBreak() throws Exception { @@ -35657,6 +35669,12 @@ public class DiagnosisCompilerTestFE10TestdataTestGenerated extends AbstractDiag runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/kt10463.kt"); } + @Test + @TestMetadata("lambdaInCallArgs.kt") + public void testLambdaInCallArgs() throws Exception { + runTest("compiler/testData/diagnostics/testsWithStdLib/smartcasts/lambdaInCallArgs.kt"); + } + @Test @TestMetadata("lazyDeclaresAndModifies.kt") public void testLazyDeclaresAndModifies() throws Exception {