[K/N] Fix a StackOverflowError in casts optimization pass The pass keeps `variableAliases`, mapping a mutable variable to the value it currently holds. It relies on an unwritten invariant: an alias target is never itself aliased, so the map is a depth-1 forest. `buildIsSubtypeOfPredicate` resolves exactly one level, while `buildNullablePredicate` and `buildBooleanPredicate` resolve transitively without a cycle guard. `visitGetValue` broke that invariant. When a read sees a different alias than it did on a previous loop iteration, no alias of the variable can be returned, and the read fell back to the variable itself - which is aliased too, and whose alias keeps changing. `setVariable` then stored that as an alias, so two nullable variables assigned to each other across nested loops closed a cycle `p -> q -> p`, and the transitive resolution recursed until the stack overflowed. Fall back to a phantom variable pinned to the read site instead. It is stable across the iterations, so the loop fixpoint still converges, and it is never aliased itself, so no chain longer than one link can be built. This is how `mergeControlFlow` already models a phi node. Besides the crash, returning the raw variable also made the assignee track the assigned variable's future values, which could fold a type check against a value the variable never held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> ^KT-88316 Fixed
diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CastsOptimization.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CastsOptimization.kt index 81973e9..3f6fd9f 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CastsOptimization.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/CastsOptimization.kt
@@ -1397,7 +1397,13 @@ currentAlias } else { getValueMergedVariableAliases[variable] = multipleValuesMarker - variable + // This read sees different aliases on different loop iterations, so no alias of [variable] can be + // returned. Returning [variable] itself is not an option either: it is aliased too, and its alias + // keeps changing, so the callers (setVariable, in particular) would store an alias of an alias. + // Two variables assigned to each other could then close a cycle in variableAliases, making the + // transitive resolution in buildNullablePredicate/buildBooleanPredicate recurse forever (KT-88316). + // A phantom pinned to this read site is stable across the iterations and is never aliased itself. + createPhantomVariable(variable, createPhantomValueAt(variable, expression)) } return VisitorResult(data, actualAlias) }
diff --git a/native/native.tests/testData/codegen/fileCheck/kt88316.kt b/native/native.tests/testData/codegen/fileCheck/kt88316.kt new file mode 100644 index 0000000..2d70a23 --- /dev/null +++ b/native/native.tests/testData/codegen/fileCheck/kt88316.kt
@@ -0,0 +1,33 @@ +// TARGET_BACKEND: NATIVE +// FILECHECK_STAGE: CStubs +// FREE_COMPILER_ARGS: -Xbinary=genericSafeCasts=true + +// KT-88316: two nullable local variables assigned to each other across nested loops made +// CastsOptimization's variable alias map cyclic (p -> q -> p): on a repeated loop iteration +// a variable read whose alias had changed returned the variable itself, even though that +// variable was aliased, and the assignment stored it as an alias. buildNullablePredicate +// follows alias chains transitively, so it recursed until the stack overflowed. + +// CHECK-LABEL: define i32 @"kfun:#foo(kotlin.String?){}kotlin.Int" +fun foo(a: String?): Int { + var count = 0 + var p = a + while (p != null) { + var q: String? = p + while (q != null) { + count++ + q = null + } + p = q + } + return count +// CHECK-LABEL: epilogue: +} + +// CHECK-LABEL: define ptr @"kfun:#box(){}kotlin.String" +fun box(): String { + if (foo(null) != 0) return "fail 1" + if (foo("x") != 1) return "fail 2" + + return "OK" +}