[FIR] Create alternate DFA flows through finally blocks

Entering a `finally` block can happen from many different places:
through an exception, a jump, or normal exit from the `try` block. When
in the `finally` block, all DFA flows must be merged to have correct
smart casting. However, after the `finally` block, if exiting normally
or because of a jump, the combined flow from within the `finally` block
should not be used, but rather an alternate flow which combines the
correct flows from before the `finally` block.

```
try {
    str as String // Potential cast exception
} finally {
    str.length // Shouldn`t be resolved
}
str.length // Should be resolved
```

When building DFA flows, track the start of possible alternate flows,
and continue building them until they end. Both of these situations are
now marked on CFGNodes via interfaces.

When building the default DFA flow, and the source node is the end node
of alternate flows, attempt to use the alternate flow with the same edge
label instead of the default flow of the source node.

#KT-56888 Fixed
This commit is contained in:
Brian Norman
2023-07-20 07:05:53 -05:00
committed by Space Team
parent f3847de1b9
commit 0e2b3ce845
18 changed files with 677 additions and 546 deletions
@@ -631,21 +631,21 @@ abstract class FirDataFlowAnalyzer(
fun exitWhileLoop(loop: FirLoop) {
val (conditionEnterNode, blockExitNode, exitNode) = graphBuilder.exitWhileLoop(loop)
blockExitNode.mergeIncomingFlow()
exitNode.mergeIncomingFlow { _, flow ->
processWhileLoopExit(flow, exitNode, conditionEnterNode)
exitNode.mergeIncomingFlow { path, flow ->
processWhileLoopExit(path, flow, exitNode, conditionEnterNode)
processLoopExit(flow, exitNode, exitNode.firstPreviousNode as LoopConditionExitNode)
}
}
private fun processWhileLoopExit(flow: MutableFlow, node: LoopExitNode, conditionEnterNode: LoopConditionEnterNode) {
private fun processWhileLoopExit(path: FlowPath, flow: MutableFlow, node: LoopExitNode, conditionEnterNode: LoopConditionEnterNode) {
val possiblyChangedVariables = exitRepeatableStatement(node.fir)
if (possiblyChangedVariables.isNullOrEmpty()) return
// 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.
val conditionEnterFlow = conditionEnterNode.flow
val loopEnterAndContinueFlows = conditionEnterNode.livePreviousFlows
val conditionExitAndBreakFlows = node.livePreviousFlows
val conditionEnterFlow = conditionEnterNode.getFlow(path)
val loopEnterAndContinueFlows = conditionEnterNode.previousLiveNodes.map { it.getFlow(path) }.toList()
val conditionExitAndBreakFlows = node.previousLiveNodes.map { it.getFlow(path) }.toList()
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.
@@ -769,7 +769,7 @@ abstract class FirDataFlowAnalyzer(
fun exitSafeCall(safeCall: FirSafeCallExpression) {
val node = graphBuilder.exitSafeCall()
node.mergeIncomingFlow { _, flow ->
node.mergeIncomingFlow { path, flow ->
// If there is only 1 previous node, then this is LHS of `a?.b ?: c`; then the null-case
// edge from `a` goes directly to `c` and this node's flow already assumes `b` executed.
if (node.previousNodes.size < 2) return@mergeIncomingFlow
@@ -779,7 +779,7 @@ abstract class FirDataFlowAnalyzer(
// TODO? all new implications in previous node's flow are valid here if receiver != null
// (that requires a second level of implications: receiver != null => condition => effect).
// KT-59689
flow.addAllConditionally(expressionVariable notEq null, node.lastPreviousNode.flow)
flow.addAllConditionally(expressionVariable notEq null, node.lastPreviousNode.getFlow(path))
}
}
@@ -1003,10 +1003,10 @@ abstract class FirDataFlowAnalyzer(
graphBuilder.exitBinaryLogicExpression(binaryLogicExpression).mergeBinaryLogicOperatorFlow()
}
private fun AbstractBinaryExitNode<FirBinaryLogicExpression>.mergeBinaryLogicOperatorFlow() = mergeIncomingFlow { _, flow ->
private fun AbstractBinaryExitNode<FirBinaryLogicExpression>.mergeBinaryLogicOperatorFlow() = mergeIncomingFlow { path, flow ->
val isAnd = fir.kind == LogicOperationKind.AND
val flowFromLeft = leftOperandNode.flow
val flowFromRight = rightOperandNode.flow
val flowFromLeft = leftOperandNode.getFlow(path)
val flowFromRight = rightOperandNode.getFlow(path)
val leftVariable = variableStorage.get(flowFromLeft, fir.leftOperand)
val leftIsBoolean = leftVariable != null && fir.leftOperand.coneType.isBoolean
@@ -1105,19 +1105,21 @@ abstract class FirDataFlowAnalyzer(
val (lhsExitNode, lhsIsNotNullNode, rhsEnterNode) = graphBuilder.exitElvisLhs(elvisExpression)
lhsExitNode.mergeIncomingFlow()
val lhsVariable = variableStorage.getOrCreateIfReal(lhsExitNode.flow, elvisExpression.lhs)
lhsIsNotNullNode.mergeIncomingFlow { _, flow ->
lhsVariable?.let { flow.commitOperationStatement(it notEq null) }
fun getLhsVariable(path: FlowPath): DataFlowVariable? =
variableStorage.getOrCreateIfReal(lhsExitNode.getFlow(path), elvisExpression.lhs)
lhsIsNotNullNode.mergeIncomingFlow { path, flow ->
getLhsVariable(path)?.let { flow.commitOperationStatement(it notEq null) }
}
rhsEnterNode.mergeIncomingFlow { _, flow ->
lhsVariable?.let { flow.commitOperationStatement(it eq null) }
rhsEnterNode.mergeIncomingFlow { path, flow ->
getLhsVariable(path)?.let { flow.commitOperationStatement(it eq null) }
}
}
@OptIn(UnexpandedTypeCheck::class)
fun exitElvis(elvisExpression: FirElvisExpression, isLhsNotNull: Boolean, callCompleted: Boolean) {
val node = graphBuilder.exitElvis(isLhsNotNull, callCompleted)
node.mergeIncomingFlow { _, flow ->
node.mergeIncomingFlow { path, flow ->
// If LHS is never null, then the edge from RHS is dead and this node's flow already contains
// all statements from LHS unconditionally.
if (isLhsNotNull) return@mergeIncomingFlow
@@ -1144,7 +1146,7 @@ abstract class FirDataFlowAnalyzer(
// implications, but for constant v the logic system can handle some basic cases of P(x).
val rhs = (elvisExpression.rhs as? FirConstExpression<*>)?.value as? Boolean
if (rhs != null) {
flow.addAllConditionally(elvisVariable eq !rhs, node.firstPreviousNode.flow)
flow.addAllConditionally(elvisVariable eq !rhs, node.firstPreviousNode.getFlow(path))
}
}
}
@@ -1161,9 +1163,6 @@ abstract class FirDataFlowAnalyzer(
// ------------------------------------------------------ Utils ------------------------------------------------------
private val CFGNode<*>.livePreviousFlows: List<PersistentFlow>
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
@@ -1177,18 +1176,39 @@ abstract class FirDataFlowAnalyzer(
path: FlowPath,
builder: (FlowPath, MutableFlow) -> Unit,
): MutableFlow {
val previousFlows = previousNodes.mapNotNull {
val incomingEdgeKind = edgeFrom(it).kind
if (if (isDead) !incomingEdgeKind.usedInDeadDfa else !incomingEdgeKind.usedInDfa) return@mapNotNull null
val previousFlows = previousDfaNodes.mapNotNull { (edge, node) ->
// For CFGNodes that cause alternate flow paths to be created, only edges with matching labels should be merged. However, when
// an alternate flow is being propagated through one of these CFGNodes - i.e., when the FirElements do not match - only
// NormalPath edges should be merged.
if (this is AlternateFlowStartMarker && path is FlowPath.CfgEdge) {
if (path.fir == this.fir && edge.label != path.label) {
return@mapNotNull null
} else if (path.fir != this.fir && edge.label != NormalPath) {
return@mapNotNull null
}
}
// `MergePostponedLambdaExitsNode` nodes form a parallel data flow graph. We never compute
// data flow for any of them until reaching a completed call.
if (it is MergePostponedLambdaExitsNode) it.mergeIncomingFlow()
if (node is MergePostponedLambdaExitsNode && !node.flowInitialized) node.mergeIncomingFlow()
it.flow
}
when (path) {
FlowPath.Default -> {
// For CFGNodes that are the end of alternate flows, use the alternate flow associated with the edge label.
if (node is AlternateFlowEndMarker) {
val alternatePath = FlowPath.CfgEdge(edge.label, node.fir)
node.getAlternateFlow(alternatePath) ?: node.flow
} else {
node.flow
}
}
else -> {
node.getAlternateFlow(path) ?: node.flow
}
}
}.toList()
val result = logicSystem.joinFlow(previousFlows, isUnion)
if (graphBuilder.lastNodeOrNull == this) {
if (path == FlowPath.Default && graphBuilder.lastNodeOrNull == this) {
// Here it is, the new `lastNode`. If the previous state is the only predecessor, then there is actually
// nothing to update; `addTypeStatement` has already ensured we have the correct information.
if (currentReceiverState == null || previousFlows.singleOrNull() != currentReceiverState) {
@@ -1203,11 +1223,42 @@ abstract class FirDataFlowAnalyzer(
private fun CFGNode<*>.mergeIncomingFlow(
builder: (FlowPath, MutableFlow) -> Unit = { _, _ -> },
) {
// Always build the default flow path for all nodes.
val mutableDefaultFlow = buildIncomingFlow(FlowPath.Default, builder)
val defaultFlow = mutableDefaultFlow.freeze().also { this.flow = it }
if (currentReceiverState === mutableDefaultFlow) {
currentReceiverState = defaultFlow
}
// Propagate alternate flows from previous nodes.
val propagatePaths = previousDfaNodes.flatMapTo(mutableSetOf()) { (edge, node) ->
when (node) {
// If the source node is the end of alternate flows, do not propagate the alternate flows which have ended.
is AlternateFlowEndMarker -> node.alternateFlowPaths.filter { it !is FlowPath.CfgEdge || it.fir != node.fir }
// Otherwise, only propagate alternate flows which originate along a normal path edge.
else -> node.alternateFlowPaths.takeIf { edge.label == NormalPath } ?: emptyList()
}
}
for (path in propagatePaths) {
addAlternateFlow(path, buildIncomingFlow(path, builder).freeze())
}
// Add any new alternate flows that should be created.
if (this is AlternateFlowStartMarker) {
val additionalPaths = previousDfaNodes
.mapNotNullTo(mutableSetOf()) { (edge, _) -> edge.label.takeIf { it != UncaughtExceptionPath } }
.map { FlowPath.CfgEdge(it, this.fir) }
for (path in additionalPaths) {
addAlternateFlow(path, buildIncomingFlow(path, builder).freeze())
}
}
}
private fun CFGNode<*>.getFlow(path: FlowPath): PersistentFlow {
return when (path) {
FlowPath.Default -> flow
else -> getAlternateFlow(path) ?: error("no alternate flow for $path")
}
}
// In rare cases (like after exiting functions) after adding more nodes `graphBuilder` will revert the current
@@ -9,8 +9,8 @@ import org.jetbrains.kotlin.fir.FirElement
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.EdgeLabel
/**
* Sealed interface representing a type of path through a [Control Flow Graph (CFG) Node][org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode]
* used for data flow analysis. Most CFG nodes only have a single data flow path through them, but there are times when a node may require
* Sealed class representing a type of path through a [Control Flow Graph (CFG) Node][org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode] used
* for data flow analysis. Most CFG nodes only have a single data flow path through them, but there are times when a node may require
* multiple paths to be calculated. The most common use case is for `finally` code blocks, where multiple code paths may enter, but these
* paths diverge after exiting the code block. Consider the following (very) contrived example:
*
@@ -56,11 +56,11 @@ import org.jetbrains.kotlin.fir.resolve.dfa.cfg.EdgeLabel
* 3. A flow which will be used when the entire `try` expression exits without exception or jumping. This data flow is a continuation of the
* data flow leading into the `finally` block from the main `try` block.
*/
sealed interface FlowPath {
sealed class FlowPath {
/**
* The [FlowPath] which represents the combination of all flows leading into a CFG Node.
*/
data object Default : FlowPath
data object Default : FlowPath()
/**
* The [FlowPath] which represents the combination of all flows leading into a CFG Node that follow an edge with the specified
@@ -68,5 +68,5 @@ sealed interface FlowPath {
* multiple flows. For example, a `finally` block within another `finally` block will require multiple
* [normal paths][org.jetbrains.kotlin.fir.resolve.dfa.cfg.NormalPath] through each that diverge at different nodes.
*/
data class CfgEdge(val label: EdgeLabel, val fir: FirElement) : FlowPath
data class CfgEdge(val label: EdgeLabel, val fir: FirElement) : FlowPath()
}
@@ -170,11 +170,22 @@ sealed class CFGNode<out E : FirElement>(val owner: ControlFlowGraph, val level:
val CFGNode<*>.firstPreviousNode: CFGNode<*> get() = previousNodes[0]
val CFGNode<*>.lastPreviousNode: CFGNode<*> get() = previousNodes.last()
val CFGNode<*>.previousDfaNodes: Sequence<Pair<Edge, CFGNode<*>>>
get() = previousNodes.asSequence()
.map { edgeFrom(it) to it }
.filter { (edge, _) -> if (isDead) edge.kind.usedInDeadDfa else edge.kind.usedInDfa }
val CFGNode<*>.previousLiveNodes: Sequence<CFGNode<*>>
get() = when {
this.isDead -> previousNodes.asSequence()
else -> previousNodes.asSequence().mapNotNull { it.takeIf { !it.isDead } }
}
interface EnterNodeMarker
interface ExitNodeMarker
interface GraphEnterNodeMarker : EnterNodeMarker
interface GraphExitNodeMarker : ExitNodeMarker
interface AlternateFlowStartMarker
interface AlternateFlowEndMarker
// ----------------------------------- EnterNode for declaration with CFG -----------------------------------
@@ -260,6 +271,17 @@ class PostponedLambdaExitNode(owner: ControlFlowGraph, override val fir: FirAnon
}
class MergePostponedLambdaExitsNode(owner: ControlFlowGraph, override val fir: FirElement, level: Int) : CFGNode<FirElement>(owner, level) {
private var _flowInitialized = false
val flowInitialized: Boolean get() = _flowInitialized
override var flow: PersistentFlow
get() = super.flow
@CfgInternals
set(value) {
super.flow = value
_flowInitialized = true
}
override fun <R, D> accept(visitor: ControlFlowGraphVisitor<R, D>, data: D): R {
return visitor.visitMergePostponedLambdaExitsNode(this, data)
}
@@ -543,13 +565,13 @@ class CatchClauseExitNode(owner: ControlFlowGraph, override val fir: FirCatch, l
}
}
class FinallyBlockEnterNode(owner: ControlFlowGraph, override val fir: FirTryExpression, level: Int) : CFGNode<FirTryExpression>(owner, level),
EnterNodeMarker {
EnterNodeMarker, AlternateFlowStartMarker {
override fun <R, D> accept(visitor: ControlFlowGraphVisitor<R, D>, data: D): R {
return visitor.visitFinallyBlockEnterNode(this, data)
}
}
class FinallyBlockExitNode(owner: ControlFlowGraph, override val fir: FirTryExpression, level: Int) : CFGNode<FirTryExpression>(owner, level),
ExitNodeMarker {
ExitNodeMarker, AlternateFlowEndMarker {
override fun <R, D> accept(visitor: ControlFlowGraphVisitor<R, D>, data: D): R {
return visitor.visitFinallyBlockExitNode(this, data)
}
@@ -0,0 +1,23 @@
// ISSUE: KT-51759
fun testBreak(b: Boolean, s: String?) {
while (b) {
val x: String?
try {
x = s ?: break
} finally {
}
x<!UNNECESSARY_NOT_NULL_ASSERTION!>!!<!>.length
}
}
fun testContinue(b: Boolean, s: String?) {
while (b) {
val x: String?
try {
x = s ?: continue
} finally {
}
x<!UNNECESSARY_NOT_NULL_ASSERTION!>!!<!>.length
}
}
@@ -1,4 +1,3 @@
// FIR_IDENTICAL
// ISSUE: KT-51759
fun testBreak(b: Boolean, s: String?) {
@@ -17,7 +17,7 @@ fun castInTryAndCatch(s: Any) {
} finally {
s.<!UNRESOLVED_REFERENCE!>length<!> // shouldn't be resolved
}
s.<!UNRESOLVED_REFERENCE!>length<!> // should be smartcast
s.length // should be smartcast
}
fun castAtAll(s: Any) {
File diff suppressed because it is too large Load Diff
@@ -26,13 +26,13 @@ fun breakInTry_withNestedFinally() {
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be ok
x.bbb() // should be ok
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be ok
x.bbb() // should be ok
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be ok
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be ok
x.aaa() // should be ok
x.bbb() // should be ok
}
fun returnInCatch() {
@@ -46,7 +46,7 @@ fun returnInCatch() {
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be ok
x.aaa() // should be ok
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
}
@@ -65,13 +65,13 @@ fun returnInCatch_insideFinally() {
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>ccc<!>() // should be error
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be ok
x.aaa() // should be ok
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>ccc<!>() // should be error
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be ok
x.aaa() // should be ok
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>ccc<!>() // should be ok
x.ccc() // should be ok
}
fun breakInCatch() {
@@ -86,11 +86,11 @@ fun breakInCatch() {
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be ok
x.aaa() // should be ok
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be error
}
x.<!UNRESOLVED_REFERENCE!>aaa<!>() // should be error
x.<!UNRESOLVED_REFERENCE!>bbb<!>() // should be ok
x.bbb() // should be ok
}
fun returnInFinally_insideTry_nonLocal() {
@@ -4,7 +4,7 @@ fun castInTry(s: Any) {
} finally {
s.<!UNRESOLVED_REFERENCE!>length<!> // Shouldn't be resolved
}
s.<!UNRESOLVED_REFERENCE!>length<!> // Shouldn't be resolved
s.length // Should be smartcast
}
fun castInTryAndFinally(s: Any) {
@@ -4,7 +4,7 @@ fun castInTry(s: Any) {
} finally {
s.<!UNRESOLVED_REFERENCE!>length<!> // Shouldn't be resolved
}
s.<!UNRESOLVED_REFERENCE!>length<!> // Shouldn't be resolved
s.<!UNRESOLVED_REFERENCE!>length<!> // Should be smartcast
}
fun castInTryAndFinally(s: Any) {
@@ -72,7 +72,7 @@ fun conditionalThrowInTry_rethrow_smartcastAfterTryCatchFinally(a: A) {
} catch (e: Throwable) {
throw e
} finally {}
takeB(<!ARGUMENT_TYPE_MISMATCH!>a<!>)
takeB(a)
}
fun conditionalThrowInTry_rethrow_noSmartcastInFinally(a: A) {
@@ -82,7 +82,7 @@ fun test5() {
s2<!UNSAFE_CALL!>.<!>length
}
s1.length
s2<!UNSAFE_CALL!>.<!>length
s2.length
}
fun test6(s1: String?, s2: String?) {
@@ -100,6 +100,6 @@ fun test6(s1: String?, s2: String?) {
requireNotNull(s2)
}
s<!UNSAFE_CALL!>.<!>length
s1<!UNSAFE_CALL!>.<!>length
s1.length
s2.length
}
@@ -84,7 +84,7 @@ fun test5() {
s2<!UNSAFE_CALL!>.<!>length
}
s1.length
s2<!UNSAFE_CALL!>.<!>length
s2.length
}
fun test6(s1: String?, s2: String?) {
@@ -102,6 +102,6 @@ fun test6(s1: String?, s2: String?) {
requireNotNull(s2)
}
s<!UNSAFE_CALL!>.<!>length
s1<!UNSAFE_CALL!>.<!>length
s1.length
s2.length
}
@@ -56,7 +56,7 @@ fun case_4() {
*/
fun case_5() {
var x: Int? = null
if (<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int?")!>x<!> == try { x = 10; null } finally {} && x != null) {
if (<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int?")!>x<!> == try { x = 10; null } finally {} && <!SENSELESS_COMPARISON!>x != null<!>) {
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int? & kotlin.Int")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int? & kotlin.Int")!>x<!>.inv()
}
@@ -52,5 +52,5 @@ fun case_4() {
try {
x = null
} finally { }
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Boolean?")!>x<!><!UNSAFE_CALL!>.<!>not()
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Boolean? & kotlin.Nothing?")!>x<!><!UNSAFE_CALL!>.<!>not()
}
@@ -57,7 +57,7 @@ fun case_3() {
*/
fun case_4() {
var x: Int? = null
if (x == try { x = 10; null } finally {} && x != null) {
if (x == try { x = 10; null } finally {} && <!SENSELESS_COMPARISON!>x != null<!>) {
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int? & kotlin.Int")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int? & kotlin.Int")!>x<!>.inv()
println(1)
@@ -39,8 +39,8 @@ fun case_2() {
try {
x = null
} finally { }
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Boolean?")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Boolean?")!>x<!><!UNSAFE_CALL!>.<!>not()
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Boolean? & kotlin.Nothing?")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Boolean? & kotlin.Nothing?")!>x<!><!UNSAFE_CALL!>.<!>not()
}
}
@@ -32,8 +32,8 @@ fun case_2() {
var x: String?
x = "Test"
println("${try { x = null } finally { }}")
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.String?")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.String?")!>x<!><!UNSAFE_CALL!>.<!>length
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.String? & kotlin.Nothing?")!>x<!>
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.String? & kotlin.Nothing?")!>x<!><!UNSAFE_CALL!>.<!>length
}
/*