diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirTailrecFunctionChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirTailrecFunctionChecker.kt index 873a1e1f3a2..2b1c9668973 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirTailrecFunctionChecker.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/FirTailrecFunctionChecker.kt @@ -20,7 +20,9 @@ import org.jetbrains.kotlin.fir.expressions.impl.FirNoReceiverExpression import org.jetbrains.kotlin.fir.expressions.toResolvedCallableSymbol import org.jetbrains.kotlin.fir.resolve.dfa.cfg.* import org.jetbrains.kotlin.fir.resolve.dfa.controlFlowGraph +import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol +import org.jetbrains.kotlin.fir.types.toSymbol object FirTailrecFunctionChecker : FirSimpleFunctionChecker() { override fun check(declaration: FirSimpleFunction, context: CheckerContext, reporter: DiagnosticReporter) { @@ -80,8 +82,8 @@ object FirTailrecFunctionChecker : FirSimpleFunctionChecker() { } val dispatchReceiver = functionCall.dispatchReceiver // A tailrec call does not support changing dispatchers. Here we report changing dispatch receiver if the dispatch receiver - // is present and not a `this`. For the `this` check, we don't need to actually compare if the dispatch receiver `this` - // references the same `this` made available from `declaration`. This is because + // is present and not a `this` or a singleton. For the `this` check, we don't need to actually compare if the dispatch + // receiver `this` references the same `this` made available from `declaration`. This is because // 1. if `this` is not labeled, then it references the innermost `this` receiver. If the innermost scope is not the // `declaration` body, then follow-up checks on following nodes would report there to be more instructions, which would // then make this call non-tailrec. @@ -93,9 +95,10 @@ object FirTailrecFunctionChecker : FirSimpleFunctionChecker() { // is already bailed out earlier. So there is no need to report anything. // c. `declaration` is a member function of an inner class and the receiver is a labeled `this` pointing to the outer // class. The reasoning is the same with b. - // Also note that if the dispatch receiver is explicitly specify to be a singleton, the call is not compiled as a tailrec - // either. See KT-48602. - if (dispatchReceiver !is FirThisReceiverExpression && dispatchReceiver !is FirNoReceiverExpression) { + if (dispatchReceiver !is FirThisReceiverExpression && + dispatchReceiver !is FirNoReceiverExpression && + (declaration.dispatchReceiverType?.toSymbol(context.session) as? FirClassSymbol<*>)?.classKind?.isSingleton != true + ) { reporter.reportOn(functionCall.source, FirErrors.NON_TAIL_RECURSIVE_CALL, context) return } diff --git a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java index e91c75892e5..a97dfc79248 100644 --- a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java +++ b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java @@ -14584,6 +14584,18 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailRecursionInFinally.kt"); } + @Test + @TestMetadata("tailrecWithExplicitCompanionObjectDispatcher.kt") + public void testTailrecWithExplicitCompanionObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt"); + } + + @Test + @TestMetadata("tailrecWithExplicitObjectDispatcher.kt") + public void testTailrecWithExplicitObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt"); + } + @Test @TestMetadata("thisReferences.kt") public void testThisReferences() throws Exception { diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/TailRecursionCallsCollector.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/TailRecursionCallsCollector.kt index fa1957567b5..cbd95fb2648 100644 --- a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/TailRecursionCallsCollector.kt +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/TailRecursionCallsCollector.kt @@ -16,11 +16,14 @@ package org.jetbrains.kotlin.backend.common +import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrFunction import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.types.classOrNull import org.jetbrains.kotlin.ir.types.isUnit import org.jetbrains.kotlin.ir.util.usesDefaultArguments import org.jetbrains.kotlin.ir.visitors.IrElementVisitor @@ -107,6 +110,13 @@ fun collectTailRecursionCalls(irFunction: IrFunction): Set { // Overridden functions using default arguments at tail call are not included: KT-4285 return } + val dispatchReceiverType = irFunction.dispatchReceiverParameter?.type + if (dispatchReceiverType?.classOrNull?.owner?.kind?.isSingleton == true) { + // Dispatch receiver type is singleton and hence it can't be changed and the call must be tailrec. + result.add(expression) + return + } + expression.dispatchReceiver?.let { if (it !is IrGetValue || it.symbol.owner != irFunction.dispatchReceiverParameter) { diff --git a/compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt b/compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt new file mode 100644 index 00000000000..5f1884f8ed6 --- /dev/null +++ b/compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt @@ -0,0 +1,25 @@ +// DONT_TARGET_EXACT_BACKEND: WASM +// WASM_MUTE_REASON: IGNORED_IN_JS +// IGNORE_BACKEND: JS_IR +// IGNORE_BACKEND: JS_IR_ES6 +// DONT_RUN_GENERATED_CODE: JS +// IGNORE_BACKEND: JS +// IGNORE_BACKEND: JVM +// IGNORE_FIR_DIAGNOSTICS_DIFF + +// Light analysis thinks this test passes but it doesn't because JVM backend does not compile this into a tailrec function. +// IGNORE_LIGHT_ANALYSIS + +class C { + companion object { + tailrec fun rec(i: Int) { + if (i <= 0) return + C.rec(i - 1) + } + } +} + +fun box(): String { + C.rec(100000) + return "OK" +} diff --git a/compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt b/compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt new file mode 100644 index 00000000000..e40ef76a240 --- /dev/null +++ b/compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt @@ -0,0 +1,16 @@ +// DONT_TARGET_EXACT_BACKEND: WASM +// WASM_MUTE_REASON: IGNORED_IN_JS +// IGNORE_BACKEND: JS_IR_ES6 +// DONT_RUN_GENERATED_CODE: JS + +object O { + tailrec fun rec(i: Int) { + if (i <= 0) return + O.rec(i - 1) + } +} + +fun box(): String { + O.rec(100000) + return "OK" +} diff --git a/compiler/testData/diagnostics/tests/tailRecSingleton.fir.kt b/compiler/testData/diagnostics/tests/tailRecSingleton.fir.kt index 79e7aa3ae55..796037d2394 100644 --- a/compiler/testData/diagnostics/tests/tailRecSingleton.fir.kt +++ b/compiler/testData/diagnostics/tests/tailRecSingleton.fir.kt @@ -7,8 +7,8 @@ object Foo { this.foo2() } - tailrec fun foo3() { - Foo.foo3() + tailrec fun foo3() { + Foo.foo3() } } @@ -22,12 +22,12 @@ class Bar { this.bar2() } - tailrec fun bar3() { - Bar.bar3() + tailrec fun bar3() { + Bar.bar3() } - tailrec fun bar4() { - Bar.Companion.bar4() + tailrec fun bar4() { + Bar.Companion.bar4() } } } diff --git a/compiler/testData/diagnostics/tests/tailRecursionComplex.fir.kt b/compiler/testData/diagnostics/tests/tailRecursionComplex.fir.kt deleted file mode 100644 index df08de4612f..00000000000 --- a/compiler/testData/diagnostics/tests/tailRecursionComplex.fir.kt +++ /dev/null @@ -1,16 +0,0 @@ -object O { - // foo is the same, but the compiler currently doesn't compile this as tail recursive. See KT-48602 - tailrec fun foo(i: Int): Int = if (i < 0) 0 else O.foo(i - 1) -} - -class A { - tailrec fun foo(i: Int) = if (i < 0) 0 else A.foo(i - 1) - - companion object { - fun foo(i: Int) = 42 + i - } -} - -class B { - tailrec fun foo(i: Int) = if (i < 0) 0 else O.foo(i - 1) -} diff --git a/compiler/testData/diagnostics/tests/tailRecursionComplex.kt b/compiler/testData/diagnostics/tests/tailRecursionComplex.kt index 865d6e0bd72..4c061db2679 100644 --- a/compiler/testData/diagnostics/tests/tailRecursionComplex.kt +++ b/compiler/testData/diagnostics/tests/tailRecursionComplex.kt @@ -1,5 +1,6 @@ +// FIR_IDENTICAL object O { - // foo is the same, but the compiler currently doesn't compile this as tail recursive. See KT-48602 + // tailrec since `O` is a singleton tailrec fun foo(i: Int): Int = if (i < 0) 0 else O.foo(i - 1) } diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java index 47b71ed85c3..da9d002bbc5 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/BlackBoxCodegenTestGenerated.java @@ -14506,6 +14506,18 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailRecursionInFinally.kt"); } + @Test + @TestMetadata("tailrecWithExplicitCompanionObjectDispatcher.kt") + public void testTailrecWithExplicitCompanionObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt"); + } + + @Test + @TestMetadata("tailrecWithExplicitObjectDispatcher.kt") + public void testTailrecWithExplicitObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt"); + } + @Test @TestMetadata("thisReferences.kt") public void testThisReferences() throws Exception { diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java index 44727d52beb..662a4d1d8b3 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java @@ -14584,6 +14584,18 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailRecursionInFinally.kt"); } + @Test + @TestMetadata("tailrecWithExplicitCompanionObjectDispatcher.kt") + public void testTailrecWithExplicitCompanionObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt"); + } + + @Test + @TestMetadata("tailrecWithExplicitObjectDispatcher.kt") + public void testTailrecWithExplicitObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt"); + } + @Test @TestMetadata("thisReferences.kt") public void testThisReferences() throws Exception { diff --git a/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java b/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java index 916d7eb8929..4adc98386a7 100644 --- a/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java +++ b/compiler/tests-gen/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java @@ -11684,6 +11684,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/recursiveCallInInlineLambdaWithCapture.kt"); } + @TestMetadata("tailrecWithExplicitCompanionObjectDispatcher.kt") + public void ignoreTailrecWithExplicitCompanionObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt"); + } + private void runTest(String testDataFilePath) throws Exception { KotlinTestUtils.runTest(this::doTest, TargetBackend.JVM, testDataFilePath); } @@ -11867,6 +11872,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailRecursionInFinally.kt"); } + @TestMetadata("tailrecWithExplicitObjectDispatcher.kt") + public void testTailrecWithExplicitObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt"); + } + @TestMetadata("thisReferences.kt") public void testThisReferences() throws Exception { runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/thisReferences.kt"); diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java index 095301a3aca..b8499348749 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java @@ -10586,6 +10586,16 @@ public class IrJsCodegenBoxES6TestGenerated extends AbstractIrJsCodegenBoxES6Tes runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailRecursionInFinally.kt"); } + @TestMetadata("tailrecWithExplicitCompanionObjectDispatcher.kt") + public void testTailrecWithExplicitCompanionObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt"); + } + + @TestMetadata("tailrecWithExplicitObjectDispatcher.kt") + public void testTailrecWithExplicitObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt"); + } + @TestMetadata("thisReferences.kt") public void testThisReferences() throws Exception { runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/thisReferences.kt"); diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java index 22034dff0a4..b984a282eb5 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java @@ -9992,6 +9992,16 @@ public class IrJsCodegenBoxTestGenerated extends AbstractIrJsCodegenBoxTest { runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailRecursionInFinally.kt"); } + @TestMetadata("tailrecWithExplicitCompanionObjectDispatcher.kt") + public void testTailrecWithExplicitCompanionObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt"); + } + + @TestMetadata("tailrecWithExplicitObjectDispatcher.kt") + public void testTailrecWithExplicitObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt"); + } + @TestMetadata("thisReferences.kt") public void testThisReferences() throws Exception { runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/thisReferences.kt"); diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java index e30d89f74ca..5af52a10121 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java @@ -9972,6 +9972,16 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest { runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailRecursionInFinally.kt"); } + @TestMetadata("tailrecWithExplicitCompanionObjectDispatcher.kt") + public void testTailrecWithExplicitCompanionObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitCompanionObjectDispatcher.kt"); + } + + @TestMetadata("tailrecWithExplicitObjectDispatcher.kt") + public void testTailrecWithExplicitObjectDispatcher() throws Exception { + runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/tailrecWithExplicitObjectDispatcher.kt"); + } + @TestMetadata("thisReferences.kt") public void testThisReferences() throws Exception { runTest("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/thisReferences.kt");