FIR CFG: filter out variables declared inside lambdas more eagerly
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
index 5db9933..c667cac 100644
--- 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
@@ -211,7 +211,7 @@
          * The generated mini CFG looks like the following, with assigned local variables annotated after each node in curly brackets.
          *
          *     ┌───────┐
-         *     │ entry │ {x y z a}
+         *     │ entry │ {x y z}
          *     └─┬─┬─┬─┘
          *       │ │ │ fallback
          *       │ │ └─────────────────────────────┐
@@ -247,11 +247,8 @@
          *
          * - 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 after
-         *   the graph is constructed, as loop closure can reintroduce them at any point. 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 "entry" and "then"
-         *   nodes. But doing that seems to be more expensive than simply letting it propagate.
+         * - `a` is not recorded anywhere because it's declared inside the lambda, and no statement needed a new block after
+         *   the declaration.
          *
          * Because names 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. This works because local variables at any scope have higher priority
@@ -260,16 +257,12 @@
         fun analyzeFunction(rootFunction: FirFunction): FirLocalVariableAssignmentAnalyzer {
             val data = MiniCfgBuilder.MiniCfgData()
             MiniCfgBuilder().visitElement(rootFunction, data)
-            for (fork in data.functionForks.values) {
-                fork.assignedInside.retainAll(fork.declaredBefore)
-            }
             return FirLocalVariableAssignmentAnalyzer(data.functionForks)
         }
 
         class FunctionFork(
-            val declaredBefore: Set<FirProperty>,
             val assignedLater: Set<FirProperty>,
-            val assignedInside: MutableSet<FirProperty>,
+            val assignedInside: Set<FirProperty>,
         )
 
         private class MiniFlow(val parents: Set<MiniFlow>) {
@@ -295,13 +288,17 @@
 
             override fun visitFunction(function: FirFunction, data: MiniCfgData) {
                 val freeVariables = data.variableDeclarations.flatMapTo(mutableSetOf()) { it.values }
-                val flowInto = data.flow.fork()
-                val flowAfter = data.flow.fork()
+                val flow = data.flow
+                // Detach the function flow so that variables declared inside don't leak into the outside.
+                val flowInto = MiniFlow.start()
                 data.flow = flowInto
                 function.acceptChildren(this, data)
-                data.flow = flowAfter
-                data.functionForks[function.symbol] =
-                    FunctionFork(freeVariables, flowAfter.assignedLater, flowInto.assignedLater)
+                flowInto.assignedLater.retainAll(freeVariables)
+                // Now that the inner variables have been discarded, the rest can be propagated to prevent smartcasts
+                // in lambdas declared before this one.
+                flow.recordAssignments(flowInto.assignedLater)
+                data.flow = flow.fork()
+                data.functionForks[function.symbol] = FunctionFork(data.flow.assignedLater, flowInto.assignedLater)
             }
 
             override fun visitWhenExpression(whenExpression: FirWhenExpression, data: MiniCfgData) {
@@ -336,14 +333,17 @@
                 data.flow = start
                 whileLoop.condition.accept(this, data)
                 whileLoop.block.accept(this, data)
-                data.flow.addBackEdgeTo(start)
+                // All forks in the loop should have the same set of variables assigned later, equal to the set
+                // at the start of the loop.
+                data.flow.recordAssignments(start.assignedLater)
             }
 
             override fun visitDoWhileLoop(doWhileLoop: FirDoWhileLoop, data: MiniCfgData) {
                 val start = data.flow.fork()
+                data.flow = start
                 doWhileLoop.block.accept(this, data)
                 doWhileLoop.condition.accept(this, data)
-                data.flow.addBackEdgeTo(start)
+                data.flow.recordAssignments(start.assignedLater)
             }
 
             // TODO: liveness analysis - return/throw/break/continue terminate the flow.
@@ -356,6 +356,8 @@
                 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.
+                    // TODO: this is not entirely correct (the lambda might be nested deep inside an expression), but also this
+                    //  entire override should be unnecessary as long as the full CFG builder visits everything in the right order
                     val (postponedFunctionArgs, normalArgs) = argumentList.arguments.partition { it is FirAnonymousFunctionExpression }
                     normalArgs.forEach { it.accept(visitor, data) }
                     postponedFunctionArgs.forEach { it.accept(visitor, data) }
@@ -392,19 +394,14 @@
             private fun MiniCfgData.recordAssignment(reference: FirReference) {
                 val name = (reference as? FirNamedReference)?.name ?: return
                 val property = variableDeclarations.lastOrNull { name in it }?.get(name) ?: return
-                flow.recordAssignments(setOf(property), mutableSetOf())
+                flow.recordAssignments(setOf(property))
             }
 
-            private fun MiniFlow.recordAssignments(properties: Set<FirProperty>, visited: MutableSet<MiniFlow>) {
-                if (!visited.add(this)) return
-                assignedLater += properties
-                parents.forEach { it.recordAssignments(properties, visited) }
-            }
-
-            private fun MiniFlow.addBackEdgeTo(loopStart: MiniFlow) {
-                // All forks in the loop should have the same set of variables assigned later, equal to the set
-                // at the start of the loop.
-                recordAssignments(loopStart.assignedLater, mutableSetOf())
+            private fun MiniFlow.recordAssignments(properties: Set<FirProperty>) {
+                // All assignments already recorded here should also have been recorded in all parents,
+                // so if (properties - assignedLater) is empty, no point in continuing.
+                if (!assignedLater.addAll(properties)) return
+                parents.forEach { it.recordAssignments(properties) }
             }
 
             class MiniCfgData {
diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.fir.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.fir.kt
index 64b8515..df1af1c2 100644
--- a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.fir.kt
+++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.fir.kt
@@ -1,4 +1,5 @@
 // ISSUE: KT-50092
+// SKIP_TXT
 
 fun test1() {
     var x: String? = "..."
@@ -53,3 +54,15 @@
         lambda<!UNNECESSARY_SAFE_CALL!>?.<!>invoke()
     }
 }
+
+fun test5() {
+    var lambda: (() -> Int)? = null
+    for (i in 1..2) {
+        lambda = {
+            var x: String?
+            x = ""
+            x.length // ok
+        }
+    }
+    lambda?.invoke()
+}
diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt
index 8698e31..4fca032 100644
--- a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt
+++ b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.kt
@@ -1,4 +1,5 @@
 // ISSUE: KT-50092
+// SKIP_TXT
 
 fun test1() {
     var x: String? = "..."
@@ -53,3 +54,15 @@
         lambda?.invoke()
     }
 }
+
+fun test5() {
+    var lambda: (() -> Int)? = null
+    for (i in 1..2) {
+        lambda = {
+            var x: String?
+            x = ""
+            <!DEBUG_INFO_SMARTCAST!>x<!>.length // ok
+        }
+    }
+    lambda?.invoke()
+}
diff --git a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.txt b/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.txt
deleted file mode 100644
index 8f14b0e..0000000
--- a/compiler/testData/diagnostics/tests/smartCasts/variables/capturedWithControlJumps.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-package
-
-public fun test1(): kotlin.Unit
-public fun test2(): kotlin.Unit
-public fun test3(): kotlin.Unit
-public fun test4(): kotlin.Unit