🍒 [K/JS] [2.4.20] Fix infinite recursion of `$suspendBridge` inside anonymous class (#6843)
Original PR: #6749
Issue: KT-86934
diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/coroutines/PrepareSuspendFunctionsForExportLowering.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/coroutines/PrepareSuspendFunctionsForExportLowering.kt
index 950f13f..1661c34 100644
--- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/coroutines/PrepareSuspendFunctionsForExportLowering.kt
+++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/coroutines/PrepareSuspendFunctionsForExportLowering.kt
@@ -13,6 +13,7 @@
import org.jetbrains.kotlin.backend.common.phaser.PhasePrerequisites
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.Modality
+import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.JsLoweredDeclarationOrigin
@@ -528,12 +529,19 @@
@PhasePrerequisites(PrepareSuspendFunctionsForExportLowering::class)
class ReplaceExportedSuspendFunctionsCallsWithTheirBridgeCall(private val context: JsIrBackendContext) : BodyLoweringPass {
override fun lower(irBody: IrBody, container: IrDeclaration) {
- if (
- container is IrSimpleFunction &&
- (container.origin == EXPORTED_SUSPEND_FUNCTION_BRIDGE || container.origin == PROMISIFIED_MEMBER_WRAPPER)
- ) return
+ // No need to replace calls inside synthetic $promisified and $suspendBridge function bodies
+ if (container is IrSimpleFunction && container.isSyntheticSuspendCallContainer)
+ return
irBody.transformChildrenVoid(object : IrElementTransformerVoid() {
+ override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement {
+ // In case of anonymous classes, we also need to exempt from replacing nested promisified and bridge function declarations
+ if (declaration.isSyntheticSuspendCallContainer)
+ return declaration
+
+ return super.visitSimpleFunction(declaration)
+ }
+
override fun visitCall(expression: IrCall): IrExpression {
// In the case of super.suspendCall, as an optimization, we can keep the call without bridging it
// since at compile time we know that it's always a Kotlin suspend function
@@ -547,6 +555,9 @@
}
})
}
+
+ private val IrSimpleFunction.isSyntheticSuspendCallContainer: Boolean
+ get() = origin == EXPORTED_SUSPEND_FUNCTION_BRIDGE || origin == PROMISIFIED_MEMBER_WRAPPER
}
/** We need to ignore exporting of the original functions only after all the exported suspend function calls
diff --git a/js/js.translator/testData/box/export/kt86934.kt b/js/js.translator/testData/box/export/kt86934.kt
new file mode 100644
index 0000000..b5f61a9
--- /dev/null
+++ b/js/js.translator/testData/box/export/kt86934.kt
@@ -0,0 +1,51 @@
+// RUN_PLAIN_BOX_FUNCTION
+// ISSUE: KT-86934
+
+// MODULE: lib
+// FILE: lib.kt
+
+@JsExport
+interface Test {
+ suspend fun func(): String
+}
+
+@JsExport
+open class Base {
+ open suspend fun func(): String = "fail"
+}
+
+@JsExport
+suspend fun main(): String {
+ // Anonymous object implementing an exported interface (virtual bridge path)
+ val viaInterface = object : Test {
+ override suspend fun func(): String = "OK"
+ }
+
+ // Anonymous object extending an open exported class (non-interface implementor bridge path)
+ val viaClass = object : Base() {
+ override suspend fun func(): String = "OK"
+ }
+
+ // Anonymous object nested inside another anonymous object's suspend method (deeper nesting)
+ val nested = object : Test {
+ override suspend fun func(): String {
+ val inner = object : Test {
+ override suspend fun func(): String = "OK"
+ }
+ return inner.func()
+ }
+ }
+
+ val r1 = viaInterface.func()
+ val r2 = viaClass.func()
+ val r3 = nested.func()
+
+ return if (r1 == "OK" && r2 == "OK" && r3 == "OK") "OK" else "fail: $r1 $r2 $r3"
+}
+
+// FILE: main.js
+async function box() {
+ var main = this.lib.main;
+
+ return await main()
+}