FIR DFA: revalidate reassigned variables after loops

If a certain type statement is true on loop entry and all continue
paths, then it is also true on exit if the condition did not reassign
the variable.

^KT-7676 tag fixed-in-k2
This commit is contained in:
pyos
2022-11-14 16:40:41 +01:00
committed by teamcity
parent f9745bd3f1
commit 02fedeb9ed
8 changed files with 83 additions and 59 deletions
@@ -19,7 +19,7 @@ interface MutableMultimap<K, V, C : Collection<V>> : Multimap<K, V, C> {
}
fun remove(key: K, value: V)
fun removeKey(key: K)
fun removeKey(key: K): C
fun clear()
}
@@ -64,8 +64,9 @@ abstract class BaseMultimap<K, V, C : Collection<V>, MC : MutableCollection<V>>
}
}
override fun removeKey(key: K) {
map.remove(key)
override fun removeKey(key: K): C {
@Suppress("UNCHECKED_CAST")
return map.remove(key) as C? ?: createEmptyContainer()
}
override fun clear() {
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.fir.resolve.dfa
import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange
import org.jetbrains.kotlin.contracts.description.canBeRevisited
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.fir.*
import org.jetbrains.kotlin.fir.contracts.FirResolvedContractDescription
@@ -27,9 +27,9 @@ import org.jetbrains.kotlin.fir.scopes.getFunctions
import org.jetbrains.kotlin.fir.scopes.impl.declaredMemberScope
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirTypeParameterSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirVariableSymbol
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.types.ConstantValueKind
@@ -40,7 +40,8 @@ class DataFlowAnalyzerContext<FLOW : Flow>(
val graphBuilder: ControlFlowGraphBuilder,
variableStorage: VariableStorageImpl,
flowOnNodes: MutableMap<CFGNode<*>, FLOW>,
val preliminaryLoopVisitor: PreliminaryLoopVisitor
val preliminaryLoopVisitor: PreliminaryLoopVisitor,
val variablesClearedBeforeLoop: Stack<List<RealVariable>>,
) {
var flowOnNodes = flowOnNodes
private set
@@ -62,6 +63,7 @@ class DataFlowAnalyzerContext<FLOW : Flow>(
flowOnNodes = mutableMapOf()
preliminaryLoopVisitor.resetState()
variablesClearedBeforeLoop.reset()
firLocalVariableAssignmentAnalyzer = null
}
@@ -69,7 +71,7 @@ class DataFlowAnalyzerContext<FLOW : Flow>(
fun <FLOW : Flow> empty(session: FirSession): DataFlowAnalyzerContext<FLOW> =
DataFlowAnalyzerContext(
ControlFlowGraphBuilder(), VariableStorageImpl(session),
mutableMapOf(), PreliminaryLoopVisitor()
mutableMapOf(), PreliminaryLoopVisitor(), stackOf()
)
}
}
@@ -229,12 +231,15 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
val (postponedLambdaEnterNode, functionEnterNode) = graphBuilder.enterAnonymousFunction(anonymousFunction)
postponedLambdaEnterNode?.mergeIncomingFlow()
val flowOnEntry = functionEnterNode.mergeIncomingFlow()
when (anonymousFunction.invocationKind) {
EventOccurrencesRange.AT_LEAST_ONCE,
EventOccurrencesRange.MORE_THAN_ONCE,
EventOccurrencesRange.UNKNOWN, null ->
enterCapturingStatement(flowOnEntry, anonymousFunction)
else -> {}
val invocationKind = anonymousFunction.invocationKind
if (invocationKind == null || invocationKind.canBeRevisited()) {
// TODO: if invocation can happen 0 times, there will be an edge from `functionEnterNode`
// to `functionExitNode`, so erasing statements here causes all information to be lost
// even though `statements from before && statements made inside the lambda` are correct.
// x = ""
// callUnknownNumberOfTimes { x = "" }
// /* x is String no matter how many times the lambda is called, but that information got lost */
enterCapturingStatement(flowOnEntry, anonymousFunction)
}
}
@@ -243,12 +248,9 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
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 -> {}
val invocationKind = anonymousFunction.invocationKind
if (invocationKind == null || invocationKind.canBeRevisited()) {
exitCapturingStatement(anonymousFunction)
}
functionExitNode.mergeIncomingFlow()
if (postponedLambdaExitNode != null) {
@@ -668,9 +670,9 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
fun enterWhileLoop(loop: FirLoop) {
val (loopEnterNode, loopConditionEnterNode) = graphBuilder.enterWhileLoop(loop)
val loopEnterFlow = loopEnterNode.mergeIncomingFlow()
enterCapturingStatement(loopEnterFlow, loop)
loopConditionEnterNode.mergeIncomingFlow()
loopEnterNode.mergeIncomingFlow()
val loopConditionEnterFlow = loopConditionEnterNode.mergeIncomingFlow()
enterCapturingStatement(loopConditionEnterFlow, loop)
}
fun exitWhileLoopCondition(loop: FirLoop) {
@@ -684,18 +686,36 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
}
fun exitWhileLoop(loop: FirLoop) {
val (blockExitNode, exitNode) = graphBuilder.exitWhileLoop(loop)
val (conditionEnterNode, blockExitNode, exitNode) = graphBuilder.exitWhileLoop(loop)
blockExitNode.mergeIncomingFlow()
exitNode.mergeLoopExitFlow()
exitCapturingStatement(loop)
val possiblyChangedVariables = exitCapturingStatement(loop)
// While analyzing the loop we might have added some backwards jumps to `conditionEnterNode` which weren't
// there at the time its flow was computed - which is why we erased all information about `possiblyChangedVariables`
// from it. Now that we have those edges, we can restore type information for the code after the loop.
if (!possiblyChangedVariables.isNullOrEmpty()) {
val conditionEnterFlow = conditionEnterNode.flow
val loopEnterAndContinueFlows = conditionEnterNode.livePreviousFlows
val conditionExitAndBreakFlows = exitNode.livePreviousFlows
possiblyChangedVariables.forEach { variable ->
// The statement about `variable` in `conditionEnterFlow` should be empty, so to obtain the new statement
// we can simply add the now-known input to whatever was inferred from nothing so long as the value is the same.
val statement = logicSystem.or(loopEnterAndContinueFlows.map { it.getTypeStatement(variable) ?: return@forEach })
?: return@forEach
for (beforeExitFlow in conditionExitAndBreakFlows) {
if (logicSystem.isSameValueIn(conditionEnterFlow, beforeExitFlow, variable)) {
beforeExitFlow.addTypeStatement(statement)
}
}
}
}
exitNode.mergeLoopExitFlow(exitNode.firstPreviousNode as LoopConditionExitNode)
}
private fun LoopExitNode.mergeLoopExitFlow() {
private fun LoopExitNode.mergeLoopExitFlow(conditionExitNode: LoopConditionExitNode) {
val flow = mergeIncomingFlow()
// Might be > 1 node or other type if there are `break`s:
val singlePreviousNode = previousNodes.singleOrNull { !it.isDead } as? LoopConditionExitNode ?: return
if (singlePreviousNode.fir.coneType.isBoolean) {
val variable = variableStorage.get(flow, singlePreviousNode.fir) ?: return
if (conditionExitNode.isDead || previousNodes.count { !it.isDead } > 1) return
if (conditionExitNode.fir.coneType.isBoolean) {
val variable = variableStorage.get(flow, conditionExitNode.fir) ?: return
flow.commitOperationStatement(variable eq false)
}
}
@@ -703,18 +723,23 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
private fun enterCapturingStatement(flow: FLOW, statement: FirStatement) {
val reassignedNames = context.preliminaryLoopVisitor.enterCapturingStatement(statement)
if (reassignedNames.isEmpty()) return
val possiblyChangedVariables = variableStorage.realVariables.filterKeys {
val fir = (it.symbol as? FirVariableSymbol<*>)?.fir ?: return@filterKeys false
fir.isVar && fir.name in reassignedNames
}.values
if (possiblyChangedVariables.isEmpty()) return
for (variable in possiblyChangedVariables) {
logicSystem.removeAllAboutVariable(flow, variable)
// TODO: only choose the innermost variable for each name
val possiblyChangedVariables = variableStorage.realVariables.values.filter {
val identifier = it.identifier
val symbol = identifier.symbol
// Non-local vars can never produce stable smart casts anyway.
identifier.dispatchReceiver == null && identifier.extensionReceiver == null &&
symbol is FirPropertySymbol && symbol.isVar && symbol.name in reassignedNames
}
for (variable in possiblyChangedVariables) {
logicSystem.recordNewAssignment(flow, variable, context.newAssignmentIndex())
}
context.variablesClearedBeforeLoop.push(possiblyChangedVariables)
}
private fun exitCapturingStatement(statement: FirStatement) {
context.preliminaryLoopVisitor.exitCapturingStatement(statement)
private fun exitCapturingStatement(statement: FirStatement): List<RealVariable>? {
if (context.preliminaryLoopVisitor.exitCapturingStatement(statement).isEmpty()) return null
return context.variablesClearedBeforeLoop.pop()
}
// ----------------------------------- Do while Loop -----------------------------------
@@ -735,7 +760,7 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
fun exitDoWhileLoop(loop: FirLoop) {
val (loopConditionExitNode, loopExitNode) = graphBuilder.exitDoWhileLoop(loop)
loopConditionExitNode.mergeIncomingFlow()
loopExitNode.mergeLoopExitFlow()
loopExitNode.mergeLoopExitFlow(loopConditionExitNode)
exitCapturingStatement(loop)
}
@@ -1217,6 +1242,9 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
private val CFGNode<*>.origin: CFGNode<*> get() = if (this is StubNode) firstPreviousNode else this
private val CFGNode<*>.livePreviousFlows: List<FLOW>
get() = previousNodes.mapNotNull { it.takeIf { this.isDead || !it.isDead }?.flow }
// Smart cast information is taken from `graphBuilder.lastNode`, but the problem with receivers specifically
// is that they also affect tower resolver's scope stack. To allow accessing members on smart casted receivers,
// we explicitly patch up the stack by calling `receiverUpdated` in a way that maintains consistency with
@@ -26,9 +26,9 @@ class PreliminaryLoopVisitor {
return reassignedVariablesPerElement[statement]
}
fun exitCapturingStatement(statement: FirStatement) {
fun exitCapturingStatement(statement: FirStatement): Set<Name> {
assert(statement is FirLoop || statement is FirClass || statement is FirFunction)
reassignedVariablesPerElement.removeKey(statement)
return reassignedVariablesPerElement.removeKey(statement)
}
fun resetState() {
@@ -786,7 +786,7 @@ class ControlFlowGraphBuilder {
return conditionExitNode to loopBlockEnterNode
}
fun exitWhileLoop(loop: FirLoop): Pair<LoopBlockExitNode, LoopExitNode> {
fun exitWhileLoop(loop: FirLoop): Triple<LoopConditionEnterNode, LoopBlockExitNode, LoopExitNode> {
levelCounter--
val loopBlockExitNode = createLoopBlockExitNode(loop)
popAndAddEdge(loopBlockExitNode)
@@ -796,7 +796,7 @@ class ControlFlowGraphBuilder {
loopExitNode.updateDeadStatus()
lastNodes.push(loopExitNode)
levelCounter--
return loopBlockExitNode to loopExitNode
return Triple(conditionEnterNode, loopBlockExitNode, loopExitNode)
}
// ----------------------------------- Do while Loop -----------------------------------
@@ -21,7 +21,6 @@ abstract class LogicSystem<FLOW : Flow>(protected val context: ConeInferenceCont
abstract fun addImplication(flow: FLOW, implication: Implication)
abstract fun addLocalVariableAlias(flow: FLOW, alias: RealVariable, underlyingVariable: RealVariable)
abstract fun recordNewAssignment(flow: FLOW, variable: RealVariable, index: Int)
abstract fun removeAllAboutVariable(flow: FLOW, variable: RealVariable)
abstract fun copyAllInformation(from: FLOW, to: FLOW)
abstract fun isSameValueIn(a: FLOW, b: FLOW, variable: RealVariable): Boolean
@@ -91,9 +90,9 @@ abstract class LogicSystem<FLOW : Flow>(protected val context: ConeInferenceCont
exactType += other.exactType
}
protected fun and(statements: Collection<TypeStatement>): TypeStatement? =
fun and(statements: Collection<TypeStatement>): TypeStatement? =
statements.singleOrNew { statements.flatMapTo(mutableSetOf()) { it.exactType } }
protected fun or(statements: Collection<TypeStatement>): TypeStatement? =
fun or(statements: Collection<TypeStatement>): TypeStatement? =
statements.singleOrNew { unifyTypes(statements.map { it.exactType })?.let { mutableSetOf(it) } ?: mutableSetOf() }
}
@@ -165,10 +165,6 @@ abstract class PersistentLogicSystem(context: ConeInferenceContext) : LogicSyste
flow.addAliases(persistentSetOf(alias), flow.unwrapVariable(underlyingVariable))
}
override fun removeAllAboutVariable(flow: PersistentFlow, variable: RealVariable) {
flow.replaceVariable(variable, null)
}
private fun PersistentFlow.replaceVariable(variable: RealVariable, replacement: RealVariable?) {
val original = directAliasMap[variable]
if (original != null) {
@@ -320,7 +316,7 @@ abstract class PersistentLogicSystem(context: ConeInferenceContext) : LogicSyste
}
override fun recordNewAssignment(flow: PersistentFlow, variable: RealVariable, index: Int) {
removeAllAboutVariable(flow, variable)
flow.replaceVariable(variable, null)
flow.assignmentIndex = flow.assignmentIndex.put(variable, index)
}
@@ -13,5 +13,5 @@ fun list(start: String) {
e = e.next()
}
// e can never be null but we do not know it
e<!UNSAFE_CALL!>.<!>hashCode()
e.hashCode()
}
@@ -18,8 +18,8 @@ fun case_1() {
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>.length
}
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>b<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>b<!>.<!UNRESOLVED_REFERENCE!>length<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>.length
}
}
@@ -40,8 +40,8 @@ fun case_2() {
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>.length
}
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>b<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>b<!>.<!UNRESOLVED_REFERENCE!>length<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>.length
}
}
@@ -106,8 +106,8 @@ fun case_5() {
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>.length
}
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>b<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any")!>b<!>.<!UNRESOLVED_REFERENCE!>length<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Any & kotlin.String")!>b<!>.length
}
}