FIR DFA: slightly refactor smart cast state tracking

Instead of always looking up smart casts on `lastNode` and separately
tracking the receiver state, simply track which flow the smart casts
belong to right now. This tracked flow is auto-advanced when new
`lastNode`s are created, but can be manually rolled back for things like
KT-63709.
This commit is contained in:
Sonya Valchuk
2024-02-16 11:25:22 +00:00
committed by Space Cloud
parent 052baf1b04
commit 4c8febf10d
5 changed files with 39 additions and 50 deletions
@@ -58,10 +58,7 @@ internal open class StubBodyResolveTransformerComponents(
override fun receiverUpdated(symbol: FirBasedSymbol<*>, info: TypeStatement?) = override fun receiverUpdated(symbol: FirBasedSymbol<*>, info: TypeStatement?) =
error("Should not be called") error("Should not be called")
override fun getTypeUsingSmartcastInfo( override fun getTypeUsingSmartcastInfo(expression: FirExpression): Pair<PropertyStability, MutableList<ConeKotlinType>>? =
expression: FirExpression,
ignoreCallArguments: Boolean,
): Pair<PropertyStability, MutableList<ConeKotlinType>>? =
null null
} }
} }
@@ -366,9 +366,8 @@ private fun BodyResolveComponents.typeFromSymbol(symbol: FirBasedSymbol<*>): Fir
fun BodyResolveComponents.transformQualifiedAccessUsingSmartcastInfo( fun BodyResolveComponents.transformQualifiedAccessUsingSmartcastInfo(
qualifiedAccessExpression: FirQualifiedAccessExpression, qualifiedAccessExpression: FirQualifiedAccessExpression,
ignoreCallArguments: Boolean,
): FirExpression { ): FirExpression {
val (stability, typesFromSmartCast) = dataFlowAnalyzer.getTypeUsingSmartcastInfo(qualifiedAccessExpression, ignoreCallArguments) val (stability, typesFromSmartCast) = dataFlowAnalyzer.getTypeUsingSmartcastInfo(qualifiedAccessExpression)
?: return qualifiedAccessExpression ?: return qualifiedAccessExpression
return transformExpressionUsingSmartcastInfo(qualifiedAccessExpression, stability, typesFromSmartCast) ?: qualifiedAccessExpression return transformExpressionUsingSmartcastInfo(qualifiedAccessExpression, stability, typesFromSmartCast) ?: qualifiedAccessExpression
@@ -377,7 +376,7 @@ fun BodyResolveComponents.transformQualifiedAccessUsingSmartcastInfo(
fun BodyResolveComponents.transformWhenSubjectExpressionUsingSmartcastInfo( fun BodyResolveComponents.transformWhenSubjectExpressionUsingSmartcastInfo(
whenSubjectExpression: FirWhenSubjectExpression, whenSubjectExpression: FirWhenSubjectExpression,
): FirExpression { ): FirExpression {
val (stability, typesFromSmartCast) = dataFlowAnalyzer.getTypeUsingSmartcastInfo(whenSubjectExpression, ignoreCallArguments = false) val (stability, typesFromSmartCast) = dataFlowAnalyzer.getTypeUsingSmartcastInfo(whenSubjectExpression)
?: return whenSubjectExpression ?: return whenSubjectExpression
return transformExpressionUsingSmartcastInfo(whenSubjectExpression, stability, typesFromSmartCast) ?: whenSubjectExpression return transformExpressionUsingSmartcastInfo(whenSubjectExpression, stability, typesFromSmartCast) ?: whenSubjectExpression
@@ -387,7 +386,7 @@ fun BodyResolveComponents.transformDesugaredAssignmentValueUsingSmartcastInfo(
expression: FirDesugaredAssignmentValueReferenceExpression, expression: FirDesugaredAssignmentValueReferenceExpression,
): FirExpression { ): FirExpression {
val (stability, typesFromSmartCast) = val (stability, typesFromSmartCast) =
dataFlowAnalyzer.getTypeUsingSmartcastInfo(expression.expressionRef.value, ignoreCallArguments = false) dataFlowAnalyzer.getTypeUsingSmartcastInfo(expression.expressionRef.value)
?: return expression ?: return expression
return transformExpressionUsingSmartcastInfo(expression, stability, typesFromSmartCast) ?: expression return transformExpressionUsingSmartcastInfo(expression, stability, typesFromSmartCast) ?: expression
@@ -344,7 +344,7 @@ private fun BodyResolveComponents.createExplicitReceiverForInvokeByCallable(
}.build().let { }.build().let {
callCompleter.completeCall(it, ResolutionMode.ReceiverResolution) callCompleter.completeCall(it, ResolutionMode.ReceiverResolution)
}.let { }.let {
transformQualifiedAccessUsingSmartcastInfo(it, ignoreCallArguments = true) transformQualifiedAccessUsingSmartcastInfo(it)
} }
} }
@@ -161,17 +161,9 @@ abstract class FirDataFlowAnalyzer(
* is **stateful** and changes as the FIR tree is navigated by [FirDataFlowAnalyzer]. * is **stateful** and changes as the FIR tree is navigated by [FirDataFlowAnalyzer].
* *
* @param expression The variable access expression. * @param expression The variable access expression.
* @param ignoreCallArguments Should be set to `true` when call argument flow should not be used for smart-casting. This is important
* because the receiver of implicit `invoke` calls is visited *after* the call arguments due to tower resolution.
*/ */
open fun getTypeUsingSmartcastInfo( open fun getTypeUsingSmartcastInfo(expression: FirExpression): Pair<PropertyStability, MutableList<ConeKotlinType>>? {
expression: FirExpression, val flow = currentSmartCastPosition ?: return null
ignoreCallArguments: Boolean,
): Pair<PropertyStability, MutableList<ConeKotlinType>>? {
// TODO(KT-64094): Consider moving logic to tower resolution instead.
val node = graphBuilder.lastNode
.let { if (ignoreCallArguments && it is FunctionCallArgumentsExitNode) it.enterNode else it }
val flow = node.flow
val variable = variableStorage.getRealVariableWithoutUnwrappingAlias(flow, expression) ?: return null val variable = variableStorage.getRealVariableWithoutUnwrappingAlias(flow, expression) ?: return null
val types = flow.getTypeStatement(variable)?.exactType?.ifEmpty { null } ?: return null val types = flow.getTypeStatement(variable)?.exactType?.ifEmpty { null } ?: return null
return variable.stability to types.toMutableList() return variable.stability to types.toMutableList()
@@ -221,7 +213,7 @@ abstract class FirDataFlowAnalyzer(
val (functionExitNode, postponedLambdaExitNode, graph) = graphBuilder.exitAnonymousFunction(function) val (functionExitNode, postponedLambdaExitNode, graph) = graphBuilder.exitAnonymousFunction(function)
functionExitNode.mergeIncomingFlow() functionExitNode.mergeIncomingFlow()
postponedLambdaExitNode?.mergeIncomingFlow() postponedLambdaExitNode?.mergeIncomingFlow()
resetReceivers() // roll back to state before function resetSmartCastPosition() // roll back to state before function
return FirControlFlowGraphReferenceImpl(graph) return FirControlFlowGraphReferenceImpl(graph)
} }
@@ -234,7 +226,7 @@ abstract class FirDataFlowAnalyzer(
} }
} }
val info = DataFlowInfo(variableStorage) val info = DataFlowInfo(variableStorage)
resetReceivers() resetSmartCastPosition()
return FirControlFlowGraphReferenceImpl(graph, info) return FirControlFlowGraphReferenceImpl(graph, info)
} }
@@ -255,7 +247,7 @@ abstract class FirDataFlowAnalyzer(
if (node != null) { if (node != null) {
node.mergeIncomingFlow() node.mergeIncomingFlow()
} else { } else {
resetReceivers() resetSmartCastPosition()
} }
graph?.completePostponedNodes() graph?.completePostponedNodes()
return graph return graph
@@ -276,7 +268,7 @@ abstract class FirDataFlowAnalyzer(
if (node != null) { if (node != null) {
node.mergeIncomingFlow() node.mergeIncomingFlow()
} else { } else {
resetReceivers() // to state before class initialization resetSmartCastPosition() // to state before class initialization
} }
graph?.completePostponedNodes() graph?.completePostponedNodes()
return graph return graph
@@ -932,9 +924,7 @@ abstract class FirDataFlowAnalyzer(
// Reset implicit receivers back to their state *before* call arguments as tower resolve will use receiver types to lookup // Reset implicit receivers back to their state *before* call arguments as tower resolve will use receiver types to lookup
// functions after call arguments have been processed. // functions after call arguments have been processed.
// TODO(KT-64094): Consider moving logic to tower resolution instead. // TODO(KT-64094): Consider moving logic to tower resolution instead.
val flow = exitNode.enterNode.flow resetSmartCastPositionTo(exitNode.enterNode.flow)
updateAllReceivers(currentReceiverState, flow)
currentReceiverState = flow
} }
} }
@@ -1338,12 +1328,10 @@ abstract class FirDataFlowAnalyzer(
// ------------------------------------------------------ Utils ------------------------------------------------------ // ------------------------------------------------------ Utils ------------------------------------------------------
// Smart cast information is taken from `graphBuilder.lastNode`, but the problem with receivers specifically // The data flow state from which type statements are taken during expression resolution.
// is that they also affect tower resolver's scope stack. To allow accessing members on smart casted receivers, // Should normally be equal to `graphBuilder.lastNode`, but one exception is between exiting call
// we explicitly patch up the stack by calling `receiverUpdated` in a way that maintains consistency with // arguments and exiting the call itself, where smart casting does not use information from the arguments.
// `getTypeUsingSmartcastInfo`; i.e. at any point between calls to this class' methods the types in the implicit private var currentSmartCastPosition: Flow? = null
// receiver stack also correspond to the data flow information attached to `graphBuilder.lastNode`.
private var currentReceiverState: Flow? = null
private fun CFGNode<*>.buildDefaultFlow( private fun CFGNode<*>.buildDefaultFlow(
builder: (FlowPath, MutableFlow) -> Unit, builder: (FlowPath, MutableFlow) -> Unit,
@@ -1375,12 +1363,13 @@ abstract class FirDataFlowAnalyzer(
val result = logicSystem.joinFlow(previousFlows, statementFlows, isUnion) val result = logicSystem.joinFlow(previousFlows, statementFlows, isUnion)
if (graphBuilder.lastNodeOrNull == this) { if (graphBuilder.lastNodeOrNull == this) {
// Here it is, the new `lastNode`. If the previous state is the only predecessor, then there is actually if (currentSmartCastPosition == null || currentSmartCastPosition != previousFlows.singleOrNull()) {
// nothing to update; `addTypeStatement` has already ensured we have the correct information. // Force-update the receiver stack as merging multiple flows might have changed receivers' type statements.
if (currentReceiverState == null || previousFlows.singleOrNull() != currentReceiverState) { resetSmartCastPositionTo(result)
updateAllReceivers(currentReceiverState, result) } else {
// Receiver stack should already be up-to-date, only need to swap the flow for explicit lookups.
currentSmartCastPosition = result
} }
currentReceiverState = result
} }
builder(FlowPath.Default, result) builder(FlowPath.Default, result)
@@ -1431,8 +1420,8 @@ abstract class FirDataFlowAnalyzer(
// Always build the default flow path for all nodes. // Always build the default flow path for all nodes.
val mutableDefaultFlow = buildDefaultFlow(builder) val mutableDefaultFlow = buildDefaultFlow(builder)
val defaultFlow = mutableDefaultFlow.freeze().also { this.flow = it } val defaultFlow = mutableDefaultFlow.freeze().also { this.flow = it }
if (currentReceiverState === mutableDefaultFlow) { if (currentSmartCastPosition === mutableDefaultFlow) {
currentReceiverState = defaultFlow currentSmartCastPosition = defaultFlow
} }
// Propagate alternate flows from previous nodes. // Propagate alternate flows from previous nodes.
@@ -1498,23 +1487,27 @@ abstract class FirDataFlowAnalyzer(
// In rare cases (like after exiting functions) after adding more nodes `graphBuilder` will revert the current // In rare cases (like after exiting functions) after adding more nodes `graphBuilder` will revert the current
// state to a previously created node, so none of the nodes it returned are `lastNode` and `mergeIncomingFlow` // state to a previously created node, so none of the nodes it returned are `lastNode` and `mergeIncomingFlow`
// will not ensure consistency. In that case an explicit call to `resetReceivers` is needed to roll back the stack // will not ensure the smart cast position is auto-advanced. In that case an explicit call to `resetSmartCastPosition`
// to that previously created node's state. // is needed to roll back to that previously created node's state.
private fun resetReceivers() { private fun resetSmartCastPosition() {
val currentFlow = graphBuilder.lastNodeOrNull?.flow resetSmartCastPositionTo(graphBuilder.lastNodeOrNull?.flow)
updateAllReceivers(currentReceiverState, currentFlow)
currentReceiverState = currentFlow
} }
private fun updateAllReceivers(from: Flow?, to: Flow?) { // This method can be used to change the smart cast state to some node that is not the one at which the graph
// builder is currently stopped. This is temporary: adding any more nodes to the graph will restart tracking
// of the current position in the graph.
private fun resetSmartCastPositionTo(flow: Flow?) {
val previous = currentSmartCastPosition
if (previous == flow) return
receiverStack.forEach { receiverStack.forEach {
variableStorage.getLocalVariable(it.boundSymbol)?.let { variable -> variableStorage.getLocalVariable(it.boundSymbol)?.let { variable ->
val newStatement = to?.getTypeStatement(variable) val newStatement = flow?.getTypeStatement(variable)
if (newStatement != from?.getTypeStatement(variable)) { if (newStatement != previous?.getTypeStatement(variable)) {
receiverUpdated(it.boundSymbol, newStatement) receiverUpdated(it.boundSymbol, newStatement)
} }
} }
} }
currentSmartCastPosition = flow
} }
private fun isSameValueIn(other: PersistentFlow, fir: FirElement, original: MutableFlow): Boolean { private fun isSameValueIn(other: PersistentFlow, fir: FirElement, original: MutableFlow): Boolean {
@@ -1528,7 +1521,7 @@ abstract class FirDataFlowAnalyzer(
private fun MutableFlow.addTypeStatement(info: TypeStatement) { private fun MutableFlow.addTypeStatement(info: TypeStatement) {
val newStatement = logicSystem.addTypeStatement(this, info) ?: return val newStatement = logicSystem.addTypeStatement(this, info) ?: return
if (newStatement.variable.isThisReference && this === currentReceiverState) { if (newStatement.variable.isThisReference && this === currentSmartCastPosition) {
receiverUpdated(newStatement.variable.identifier.symbol, newStatement) receiverUpdated(newStatement.variable.identifier.symbol, newStatement)
} }
} }
@@ -177,7 +177,7 @@ open class FirExpressionsResolveTransformer(transformer: FirAbstractBodyResolveT
when (result) { when (result) {
is FirQualifiedAccessExpression -> { is FirQualifiedAccessExpression -> {
dataFlowAnalyzer.exitQualifiedAccessExpression(result) dataFlowAnalyzer.exitQualifiedAccessExpression(result)
result = components.transformQualifiedAccessUsingSmartcastInfo(result, ignoreCallArguments = false) result = components.transformQualifiedAccessUsingSmartcastInfo(result)
if (result is FirSmartCastExpression) { if (result is FirSmartCastExpression) {
dataFlowAnalyzer.exitSmartCastExpression(result) dataFlowAnalyzer.exitSmartCastExpression(result)
} }