From 0878049f151b12f1bc7c9c177964b6b39304f36e Mon Sep 17 00:00:00 2001 From: Denis Zharkov Date: Wed, 1 Feb 2017 16:46:36 +0300 Subject: [PATCH] Support tailrec suspend functions in JVM backend The main problem is that inside a state machine for a named suspend function parameters of it's owner are available as a usual captured closure parameters (i.e. through synthetic fields), while TailRecursion codegen expects that parameters are straight local variables. So, the solution is just to define local var for each of real parameters (all but the last continuation parameters) for tailrec functions. #KT-15759 Fixed --- .../kotlin/codegen/ExpressionCodegen.java | 12 ++--- .../kotlin/codegen/TailRecursionCodegen.java | 8 +++- .../codegen/coroutines/CoroutineCodegen.kt | 46 +++++++++++-------- .../coroutines/featureIntersection/tailrec.kt | 35 ++++++++++++++ .../featureIntersection/tailrec.txt | 32 +++++++++++++ .../ir/IrBlackBoxCodegenTestGenerated.java | 15 ++++++ .../codegen/BlackBoxCodegenTestGenerated.java | 15 ++++++ ...LightAnalysisModeCodegenTestGenerated.java | 15 ++++++ .../semantics/JsCodegenBoxTestGenerated.java | 21 +++++++++ 9 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt create mode 100644 compiler/testData/codegen/light-analysis/coroutines/featureIntersection/tailrec.txt diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java index 871377e6386..9801257d07d 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java @@ -2497,14 +2497,14 @@ public class ExpressionCodegen extends KtVisitor impleme } public int lookupLocalIndex(DeclarationDescriptor descriptor) { - return myFrameMap.getIndex(getParameterSynonymOrThis(descriptor)); - } - - private DeclarationDescriptor getParameterSynonymOrThis(DeclarationDescriptor descriptor) { - if (!(descriptor instanceof ValueParameterDescriptor)) return descriptor; + int index = myFrameMap.getIndex(descriptor); + if (index != -1) return index; + if (!(descriptor instanceof ValueParameterDescriptor)) return -1; DeclarationDescriptor synonym = bindingContext.get(CodegenBinding.PARAMETER_SYNONYM, (ValueParameterDescriptor) descriptor); - return synonym != null ? synonym : descriptor; + if (synonym == null) return -1; + + return myFrameMap.getIndex(synonym); } @NotNull diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/TailRecursionCodegen.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/TailRecursionCodegen.java index 02a4e7bc616..8538efa9e12 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/TailRecursionCodegen.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/TailRecursionCodegen.java @@ -20,6 +20,7 @@ import com.google.common.collect.Lists; import org.jetbrains.annotations.NotNull; import org.jetbrains.kotlin.cfg.TailRecursionKind; import org.jetbrains.kotlin.codegen.context.MethodContext; +import org.jetbrains.kotlin.codegen.coroutines.CoroutineCodegenUtilKt; import org.jetbrains.kotlin.codegen.state.GenerationState; import org.jetbrains.kotlin.descriptors.CallableDescriptor; import org.jetbrains.kotlin.descriptors.FunctionDescriptor; @@ -65,7 +66,7 @@ public class TailRecursionCodegen { } public void generateTailRecursion(ResolvedCall resolvedCall) { - CallableDescriptor fd = resolvedCall.getResultingDescriptor(); + CallableDescriptor fd = CoroutineCodegenUtilKt.unwrapInitialDescriptorForSuspendFunction(resolvedCall.getResultingDescriptor()); assert fd instanceof FunctionDescriptor : "Resolved call doesn't refer to the function descriptor: " + fd; CallableMethod callable = (CallableMethod) codegen.resolveToCallable((FunctionDescriptor) fd, false, resolvedCall); @@ -73,6 +74,11 @@ public class TailRecursionCodegen { if (arguments == null) { throw new IllegalStateException("Failed to arrange value arguments by index: " + fd); } + + if (((FunctionDescriptor) fd).isSuspend()) { + AsmUtil.pop(v, callable.getValueParameters().get(callable.getValueParameters().size() - 1).getAsmType()); + } + assignParameterValues(fd, callable, arguments); if (callable.getExtensionReceiverType() != null) { if (resolvedCall.getExtensionReceiver() != fd.getExtensionReceiverParameter().getValue()) { diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineCodegen.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineCodegen.kt index 3db52687c25..974b6d11b4a 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineCodegen.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineCodegen.kt @@ -27,7 +27,6 @@ import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.annotations.Annotations import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl -import org.jetbrains.kotlin.incremental.components.NoLookupLocation import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.KtDeclarationWithBody @@ -41,11 +40,10 @@ import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin import org.jetbrains.kotlin.resolve.jvm.diagnostics.OtherOrigin import org.jetbrains.kotlin.resolve.jvm.jvmSignature.JvmMethodSignature import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.KotlinTypeFactory -import org.jetbrains.kotlin.types.typeUtil.asTypeProjection import org.jetbrains.kotlin.types.typeUtil.makeNullable import org.jetbrains.kotlin.utils.addToStdlib.safeAs import org.jetbrains.kotlin.utils.singletonOrEmptyList +import org.jetbrains.org.objectweb.asm.Label import org.jetbrains.org.objectweb.asm.MethodVisitor import org.jetbrains.org.objectweb.asm.Opcodes import org.jetbrains.org.objectweb.asm.Type @@ -114,7 +112,7 @@ class CoroutineCodegen private constructor( } override fun generateClosureBody() { - for (parameter in allLambdaParameters()) { + for (parameter in allFunctionParameters()) { val fieldInfo = parameter.getFieldInfoForCoroutineLambdaParameter() v.newField( OtherOrigin(parameter), @@ -153,7 +151,7 @@ class CoroutineCodegen private constructor( } }) - if (allLambdaParameters().size <= 1) { + if (allFunctionParameters().size <= 1) { val delegate = typeMapper.mapSignatureSkipGeneric(createCoroutineDescriptor).asmMethod val bridgeParameters = (1..delegate.argumentTypes.size - 1).map { AsmTypes.OBJECT_TYPE } + delegate.argumentTypes.last() @@ -239,7 +237,7 @@ class CoroutineCodegen private constructor( } // load resultContinuation - load(allLambdaParameters().map { typeMapper.mapType(it.type).size }.sum() + 1, AsmTypes.OBJECT_TYPE) + load(allFunctionParameters().map { typeMapper.mapType(it.type).size }.sum() + 1, AsmTypes.OBJECT_TYPE) invokespecial(owner.internalName, constructorToUseFromInvoke.name, constructorToUseFromInvoke.descriptor, false) @@ -248,7 +246,7 @@ class CoroutineCodegen private constructor( // Pass lambda parameters to 'invoke' call on newly constructed object var index = 1 - for (parameter in allLambdaParameters()) { + for (parameter in allFunctionParameters()) { val fieldInfoForCoroutineLambdaParameter = parameter.getFieldInfoForCoroutineLambdaParameter() load(index, fieldInfoForCoroutineLambdaParameter.fieldType) AsmUtil.genAssignInstanceFieldFromParam(fieldInfoForCoroutineLambdaParameter, index, this, cloneIndex) @@ -261,24 +259,32 @@ class CoroutineCodegen private constructor( } private fun ExpressionCodegen.initializeCoroutineParameters() { - for (parameter in allLambdaParameters()) { - val mappedType = typeMapper.mapType(parameter.type) - val newIndex = myFrameMap.enter(parameter, mappedType) + if (!isSuspendLambda && !originalSuspendFunctionDescriptor.isTailrec) return + for (parameter in allFunctionParameters()) { + val fieldStackValue = + if (isSuspendLambda) + StackValue.field( + parameter.getFieldInfoForCoroutineLambdaParameter(), generateThisOrOuter(context.thisDescriptor, false) + ) + else + closureContext.lookupInContext(parameter, null, state, /* ignoreNoOuter = */ false) - generateLoadField(parameter.getFieldInfoForCoroutineLambdaParameter()) + val mappedType = typeMapper.mapType(parameter.type) + fieldStackValue.put(mappedType, v) + + val newIndex = myFrameMap.enter(parameter, mappedType) v.store(newIndex, mappedType) } + + // necessary for proper tailrec codegen + val actualMethodStartLabel = Label() + v.visitLabel(actualMethodStartLabel) + context.setMethodStartLabel(actualMethodStartLabel) } - private fun allLambdaParameters() = - if (isSuspendLambda) - originalSuspendFunctionDescriptor.extensionReceiverParameter.singletonOrEmptyList() + originalSuspendFunctionDescriptor.valueParameters.orEmpty() - else - emptyList() - - private fun ExpressionCodegen.generateLoadField(fieldInfo: FieldInfo) { - StackValue.field(fieldInfo, generateThisOrOuter(context.thisDescriptor, false)).put(fieldInfo.fieldType, v) - } + private fun allFunctionParameters() = + originalSuspendFunctionDescriptor.extensionReceiverParameter.singletonOrEmptyList() + + originalSuspendFunctionDescriptor.valueParameters.orEmpty() private fun ParameterDescriptor.getFieldInfoForCoroutineLambdaParameter() = createHiddenFieldInfo(type, COROUTINE_LAMBDA_PARAMETER_PREFIX + (this.safeAs()?.index ?: "")) diff --git a/compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt b/compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt new file mode 100644 index 00000000000..e2c8cc488dd --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt @@ -0,0 +1,35 @@ +// WITH_RUNTIME +// WITH_COROUTINES +// IGNORE_BACKEND: JS +import kotlin.coroutines.experimental.* +import kotlin.coroutines.experimental.intrinsics.* +import kotlin.test.assertEquals + +suspend fun ArrayList.yield(v: Int): Unit = suspendCoroutineOrReturn { x -> + this.add(v) + x.resume(Unit) + COROUTINE_SUSPENDED +} + +tailrec suspend fun ArrayList.fromTo(from: Int, to: Int) { + if (from > to) return + yield(from) + return fromTo(from + 1, to) +} + +fun builder(c: suspend () -> Unit) { + c.startCoroutine(EmptyContinuation) +} + +fun box(): String { + val result = arrayListOf() + + builder { + result.fromTo(1, 5) + } + + assertEquals(listOf(1, 2, 3, 4, 5), result) + + return "OK" +} + diff --git a/compiler/testData/codegen/light-analysis/coroutines/featureIntersection/tailrec.txt b/compiler/testData/codegen/light-analysis/coroutines/featureIntersection/tailrec.txt new file mode 100644 index 00000000000..04fea26e23a --- /dev/null +++ b/compiler/testData/codegen/light-analysis/coroutines/featureIntersection/tailrec.txt @@ -0,0 +1,32 @@ +@kotlin.Metadata +public final class CoroutineUtilKt { + public final static @org.jetbrains.annotations.NotNull method handleExceptionContinuation(@org.jetbrains.annotations.NotNull p0: kotlin.jvm.functions.Function1): kotlin.coroutines.experimental.Continuation + public final static @org.jetbrains.annotations.NotNull method handleResultContinuation(@org.jetbrains.annotations.NotNull p0: kotlin.jvm.functions.Function1): kotlin.coroutines.experimental.Continuation +} + +@kotlin.Metadata +public class EmptyContinuation { + public final static field Companion: EmptyContinuation.Companion + private final @org.jetbrains.annotations.NotNull field context: kotlin.coroutines.experimental.CoroutineContext + inner class EmptyContinuation/Companion + public @synthetic.kotlin.jvm.GeneratedByJvmOverloads method (): void + public method (@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.CoroutineContext): void + public synthetic method (p0: kotlin.coroutines.experimental.CoroutineContext, p1: int, p2: kotlin.jvm.internal.DefaultConstructorMarker): void + public @org.jetbrains.annotations.NotNull method getContext(): kotlin.coroutines.experimental.CoroutineContext + public method resume(@org.jetbrains.annotations.Nullable p0: java.lang.Object): void + public method resumeWithException(@org.jetbrains.annotations.NotNull p0: java.lang.Throwable): void +} + +@kotlin.Metadata +public final static class EmptyContinuation/Companion { + inner class EmptyContinuation/Companion + private method (): void +} + +@kotlin.Metadata +public final class TailrecKt { + public final static @org.jetbrains.annotations.NotNull method box(): java.lang.String + public final static method builder(@org.jetbrains.annotations.NotNull p0: kotlin.jvm.functions.Function1): void + public final static @org.jetbrains.annotations.Nullable method fromTo(@org.jetbrains.annotations.NotNull p0: java.util.ArrayList, p1: int, p2: int, @org.jetbrains.annotations.NotNull p3: kotlin.coroutines.experimental.Continuation): java.lang.Object + public final static @org.jetbrains.annotations.Nullable method yield(@org.jetbrains.annotations.NotNull p0: java.util.ArrayList, p1: int, @org.jetbrains.annotations.NotNull p2: kotlin.coroutines.experimental.Continuation): java.lang.Object +} diff --git a/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java b/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java index c34b23342d4..7ae71e33d7c 100644 --- a/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java +++ b/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java @@ -5042,6 +5042,21 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes } } + @TestMetadata("compiler/testData/codegen/box/coroutines/featureIntersection") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class FeatureIntersection extends AbstractIrBlackBoxCodegenTest { + public void testAllFilesPresentInFeatureIntersection() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/featureIntersection"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM, true); + } + + @TestMetadata("tailrec.kt") + public void testTailrec() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt"); + doTest(fileName); + } + } + @TestMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index 63fdae54dc4..bd95d34fdc7 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -5042,6 +5042,21 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { } } + @TestMetadata("compiler/testData/codegen/box/coroutines/featureIntersection") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class FeatureIntersection extends AbstractBlackBoxCodegenTest { + public void testAllFilesPresentInFeatureIntersection() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/featureIntersection"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM, true); + } + + @TestMetadata("tailrec.kt") + public void testTailrec() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt"); + doTest(fileName); + } + } + @TestMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeCodegenTestGenerated.java index 0521fc8b1c1..0f8eddedcd6 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeCodegenTestGenerated.java @@ -5042,6 +5042,21 @@ public class LightAnalysisModeCodegenTestGenerated extends AbstractLightAnalysis } } + @TestMetadata("compiler/testData/codegen/box/coroutines/featureIntersection") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class FeatureIntersection extends AbstractLightAnalysisModeCodegenTest { + public void testAllFilesPresentInFeatureIntersection() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/featureIntersection"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM, true); + } + + @TestMetadata("tailrec.kt") + public void testTailrec() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt"); + doTest(fileName); + } + } + @TestMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java index 67a9f8fe5ae..823ce425824 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java @@ -5727,6 +5727,27 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest { } } + @TestMetadata("compiler/testData/codegen/box/coroutines/featureIntersection") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class FeatureIntersection extends AbstractJsCodegenBoxTest { + public void testAllFilesPresentInFeatureIntersection() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/featureIntersection"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JS, true); + } + + @TestMetadata("tailrec.kt") + public void testTailrec() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/featureIntersection/tailrec.kt"); + try { + doTest(fileName); + } + catch (Throwable ignore) { + return; + } + throw new AssertionError("Looks like this test can be unmuted. Remove IGNORE_BACKEND directive for that."); + } + } + @TestMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)