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
This commit is contained in:
Denis Zharkov
2017-02-01 16:46:36 +03:00
parent d7403ca185
commit 0878049f15
9 changed files with 172 additions and 27 deletions
@@ -2497,14 +2497,14 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> 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
@@ -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()) {
@@ -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<ValueParameterDescriptor>()?.index ?: ""))
@@ -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<Int>.yield(v: Int): Unit = suspendCoroutineOrReturn { x ->
this.add(v)
x.resume(Unit)
COROUTINE_SUSPENDED
}
tailrec suspend fun ArrayList<Int>.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<Int>()
builder {
result.fromTo(1, 5)
}
assertEquals(listOf(1, 2, 3, 4, 5), result)
return "OK"
}
@@ -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 <init>(): void
public method <init>(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.CoroutineContext): void
public synthetic method <init>(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 <init>(): 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
}
@@ -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)
@@ -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)
@@ -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)
@@ -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)