Support new strategy for suspend inline functions

The main idea is to leave all the inline functions as is, without
state machines (but keeping suspend-calls markers) and
determine whether we need a state machine from the bytecode
after inlining into a non-inline function

 #KT-17585 In Progress
 #KT-16603 In Progress
 #KT-16448 Fixed
This commit is contained in:
Denis Zharkov
2017-05-04 11:48:55 +03:00
parent 5d0aeb4ef6
commit 5b5f612a7c
20 changed files with 612 additions and 164 deletions
@@ -1184,7 +1184,6 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
@Nullable
private StackValue genCoroutineInstanceForSuspendLambda(@NotNull FunctionDescriptor suspendFunction) {
if (!CoroutineCodegenUtilKt.isStateMachineNeeded(suspendFunction, bindingContext)) return null;
if (!(suspendFunction instanceof AnonymousFunctionDescriptor)) return null;
ClassDescriptor suspendLambdaClassDescriptor = bindingContext.get(CodegenBinding.CLASS_FOR_CALLABLE, suspendFunction);
@@ -2138,9 +2137,9 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
@NotNull CallGenerator callGenerator,
@NotNull ArgumentGenerator argumentGenerator
) {
boolean isSuspensionPoint = CoroutineCodegenUtilKt.isSuspensionPointInStateMachine(resolvedCall, bindingContext);
boolean isSuspendCall = CoroutineCodegenUtilKt.isSuspendNoInlineCall(resolvedCall);
boolean isConstructor = resolvedCall.getResultingDescriptor() instanceof ConstructorDescriptor;
putReceiverAndInlineMarkerIfNeeded(callableMethod, resolvedCall, receiver, isSuspensionPoint, isConstructor);
putReceiverAndInlineMarkerIfNeeded(callableMethod, resolvedCall, receiver, isSuspendCall, isConstructor);
callGenerator.processAndPutHiddenParameters(false);
@@ -2171,7 +2170,7 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
}
}
if (isSuspensionPoint) {
if (isSuspendCall) {
v.invokestatic(
CoroutineCodegenUtilKt.COROUTINE_MARKER_OWNER,
CoroutineCodegenUtilKt.BEFORE_SUSPENSION_POINT_MARKER_NAME,
@@ -2181,7 +2180,7 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
callGenerator.genCall(callableMethod, resolvedCall, defaultMaskWasGenerated, this);
if (isSuspensionPoint) {
if (isSuspendCall) {
v.invokestatic(
CoroutineCodegenUtilKt.COROUTINE_MARKER_OWNER,
CoroutineCodegenUtilKt.AFTER_SUSPENSION_POINT_MARKER_NAME, "()V", false);
@@ -2199,12 +2198,12 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
@NotNull Callable callableMethod,
@NotNull ResolvedCall<?> resolvedCall,
@NotNull StackValue receiver,
boolean isSuspensionPoint,
boolean isSuspendCall,
boolean isConstructor
) {
boolean isSafeCallOrOnStack = receiver instanceof StackValue.SafeCall || receiver instanceof StackValue.OnStack;
if (isSuspensionPoint && !isSafeCallOrOnStack) {
if (isSuspendCall && !isSafeCallOrOnStack) {
// Inline markers are used to spill the stack before coroutine suspension
addInlineMarker(v, true);
}
@@ -2231,7 +2230,7 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> impleme
// The problem is that the stack before the call is not restored in case of null receiver.
// The solution is to spill stack just after receiver is loaded (after IFNULL) in case of safe call.
// But the problem is that we should leave the receiver itself on the stack, so we store it in a temporary variable.
if (isSuspensionPoint && isSafeCallOrOnStack) {
if (isSuspendCall && isSafeCallOrOnStack) {
boolean bothReceivers =
receiver instanceof StackValue.CallReceiver
&& ((StackValue.CallReceiver) receiver).getDispatchReceiver().type.getSort() != Type.VOID
@@ -131,7 +131,7 @@ public class FunctionCodegen {
if (owner.getContextKind() != OwnerKind.DEFAULT_IMPLS || function.hasBody()) {
FunctionGenerationStrategy strategy;
if (functionDescriptor.isSuspend()) {
if (functionDescriptor.isSuspend() && !functionDescriptor.isInline()) {
strategy = new SuspendFunctionGenerationStrategy(
state,
CoroutineCodegenUtilKt.<FunctionDescriptor>unwrapInitialDescriptorForSuspendFunction(functionDescriptor),
@@ -103,6 +103,20 @@ class CodegenAnnotatingVisitor extends KtVisitorVoid {
return recordClassForCallable(element, callableDescriptor, supertypes, name, null);
}
@NotNull
private ClassDescriptor recordClassForFunction(
@NotNull KtElement element,
@NotNull FunctionDescriptor functionDescriptor,
@NotNull String name,
@Nullable DeclarationDescriptor customContainer
) {
return recordClassForCallable(
element, functionDescriptor,
runtimeTypes.getSupertypesForClosure(functionDescriptor),
name, customContainer
);
}
@NotNull
private ClassDescriptor recordClassForCallable(
@NotNull KtElement element,
@@ -454,18 +468,23 @@ class CodegenAnnotatingVisitor extends KtVisitorVoid {
jvmSuspendFunctionView
);
if (CoroutineCodegenUtilKt.containsNonTailSuspensionCalls(functionDescriptor, bindingContext)) {
if (nameForClassOrPackageMember != null) {
nameStack.push(nameForClassOrPackageMember);
}
processNamedFunctionWithClosure(function, functionDescriptor, functionDescriptor).setSuspend(true);
if (nameForClassOrPackageMember != null) {
nameStack.pop();
}
return;
if (nameForClassOrPackageMember != null) {
nameStack.push(nameForClassOrPackageMember);
}
String name = inventAnonymousClassName();
ClassDescriptor classDescriptor =
recordClassForFunction(function, functionDescriptor, name, functionDescriptor);
MutableClosure closure = recordClosure(classDescriptor, name);
closure.setSuspend(true);
super.visitNamedFunction(function);
if (nameForClassOrPackageMember != null) {
nameStack.pop();
}
return;
}
if (nameForClassOrPackageMember != null) {
@@ -474,29 +493,18 @@ class CodegenAnnotatingVisitor extends KtVisitorVoid {
nameStack.pop();
}
else {
processNamedFunctionWithClosure(function, functionDescriptor, null);
String name = inventAnonymousClassName();
ClassDescriptor classDescriptor = recordClassForFunction(function, functionDescriptor, name, null);
recordClosure(classDescriptor, name);
classStack.push(classDescriptor);
nameStack.push(name);
super.visitNamedFunction(function);
nameStack.pop();
classStack.pop();
}
}
private MutableClosure processNamedFunctionWithClosure(
@NotNull KtNamedFunction function,
@NotNull FunctionDescriptor functionDescriptor,
@Nullable DeclarationDescriptor customContainer
) {
String name = inventAnonymousClassName();
Collection<KotlinType> supertypes = runtimeTypes.getSupertypesForClosure(functionDescriptor);
ClassDescriptor classDescriptor = recordClassForCallable(function, functionDescriptor, supertypes, name, customContainer);
MutableClosure closure = recordClosure(classDescriptor, name);
classStack.push(classDescriptor);
nameStack.push(name);
super.visitNamedFunction(function);
nameStack.pop();
classStack.pop();
return closure;
}
@Nullable
private String getNameForClassOrPackageMember(@NotNull DeclarationDescriptor descriptor) {
DeclarationDescriptor containingDeclaration = descriptor.getContainingDeclaration();
@@ -21,12 +21,12 @@ import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.ClassBuilder
import org.jetbrains.kotlin.codegen.StackValue
import org.jetbrains.kotlin.codegen.TransformationMethodVisitor
import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil
import org.jetbrains.kotlin.codegen.optimization.DeadCodeEliminationMethodTransformer
import org.jetbrains.kotlin.codegen.optimization.FixStackWithLabelNormalizationMethodTransformer
import org.jetbrains.kotlin.codegen.optimization.common.StrictBasicValue
import org.jetbrains.kotlin.codegen.optimization.common.analyzeLiveness
import org.jetbrains.kotlin.codegen.optimization.common.insnListOf
import org.jetbrains.kotlin.codegen.optimization.common.removeEmptyCatchBlocks
import org.jetbrains.kotlin.codegen.optimization.common.*
import org.jetbrains.kotlin.codegen.optimization.fixStack.top
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin
import org.jetbrains.kotlin.utils.sure
@@ -36,6 +36,8 @@ import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
import org.jetbrains.org.objectweb.asm.tree.*
import org.jetbrains.org.objectweb.asm.tree.analysis.SourceInterpreter
import org.jetbrains.org.objectweb.asm.tree.analysis.SourceValue
class CoroutineTransformerMethodVisitor(
delegate: MethodVisitor,
@@ -45,15 +47,35 @@ class CoroutineTransformerMethodVisitor(
signature: String?,
exceptions: Array<out String>?,
private val containingClassInternalName: String,
private val classBuilderForCoroutineState: ClassBuilder,
isForNamedFunction: Boolean
obtainClassBuilderForCoroutineState: () -> ClassBuilder,
private val isForNamedFunction: Boolean,
// It's only matters for named functions, may differ from '!isStatic(access)' in case of DefaultImpls
private val needDispatchReceiver: Boolean = false,
// May differ from containingClassInternalName in case of DefaultImpls
private val internalNameForDispatchReceiver: String? = null
) : TransformationMethodVisitor(delegate, access, name, desc, signature, exceptions) {
private val classBuilderForCoroutineState: ClassBuilder by lazy(obtainClassBuilderForCoroutineState)
private val continuationIndex = if (isForNamedFunction) getLastParameterIndex(desc, access) else 0
private val dataIndex = continuationIndex + 1
private val exceptionIndex = dataIndex + 1
var dataIndex = if (isForNamedFunction) -1 else 1
var exceptionIndex = if (isForNamedFunction) -1 else 2
override fun performTransformations(methodNode: MethodNode) {
val suspensionPoints = collectSuspensionPoints(methodNode)
if (isForNamedFunction) {
if (allSuspensionPointsAreTailCalls(containingClassInternalName, methodNode, suspensionPoints)) {
dropSuspensionMarkers(methodNode, suspensionPoints)
return
}
dataIndex = methodNode.maxLocals++
exceptionIndex = methodNode.maxLocals++
prepareMethodNodePreludeForNamedFunction(methodNode)
}
val customCoroutineStart = run {
val customCoroutineStartMarker = methodNode.instructions.toArray().filterIsInstance<MethodInsnNode>().firstOrNull {
it.owner == COROUTINE_MARKER_OWNER && it.name == ACTUAL_COROUTINE_START_MARKER_NAME
@@ -64,8 +86,6 @@ class CoroutineTransformerMethodVisitor(
}
}
val suspensionPoints = collectSuspensionPoints(methodNode)
for (suspensionPoint in suspensionPoints) {
splitTryCatchBlocksContainingSuspensionPoint(methodNode, suspensionPoint)
}
@@ -125,6 +145,87 @@ class CoroutineTransformerMethodVisitor(
}
private fun prepareMethodNodePreludeForNamedFunction(methodNode: MethodNode) {
val objectTypeForState = Type.getObjectType(classBuilderForCoroutineState.thisName)
methodNode.instructions.insert(withInstructionAdapter {
val createStateInstance = Label()
val storeStateObject = Label()
// We have to distinguish the following situations:
// - Our function got called in a common way (e.g. from another function or via recursive call) and we should execute our
// code from the beginning
// - We got called from `doResume` of our continuation, i.e. we need to continue from the last suspension point
//
// Also in the first case we wrap the completion into a special anonymous class instance (let's call it X$1)
// that we'll use as a continuation argument for suspension points
//
// How we distinguish the cases:
// - If the continuation is not an instance of X$1 we know exactly it's not the second case, because when resuming
// the continuation we pass an instance of that class
// - Otherwise it's still can be a recursive call. To check it's not the case we set the last bit in the label in
// `doResume` just before calling the suspend function (see kotlin.coroutines.experimental.jvm.internal.CoroutineImplForNamedFunction).
// So, if it's set we're in continuation.
visitVarInsn(Opcodes.ALOAD, continuationIndex)
instanceOf(objectTypeForState)
ifeq(createStateInstance)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
invokevirtual(
COROUTINE_IMPL_FOR_NAMED_ASM_TYPE.internalName,
"checkAndFlushLastBit", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false
)
ifeq(createStateInstance)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
goTo(storeStateObject)
visitLabel(createStateInstance)
anew(objectTypeForState)
dup()
val parameterTypesAndIndices =
getParameterTypesIndicesForCoroutineConstructor(
methodNode.desc,
methodNode.access,
needDispatchReceiver, internalNameForDispatchReceiver ?: containingClassInternalName
)
for ((type, index) in parameterTypesAndIndices) {
load(index, type)
}
invokespecial(
classBuilderForCoroutineState.thisName,
"<init>",
Type.getMethodDescriptor(
Type.VOID_TYPE,
*getParameterTypesForCoroutineConstructor(
methodNode.desc, needDispatchReceiver,
internalNameForDispatchReceiver ?: containingClassInternalName
)
),
false
)
visitLabel(storeStateObject)
visitVarInsn(Opcodes.ASTORE, continuationIndex)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
getfield(COROUTINE_IMPL_FOR_NAMED_ASM_TYPE.internalName, "data", AsmTypes.OBJECT_TYPE.descriptor)
visitVarInsn(Opcodes.ASTORE, dataIndex)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
getfield(COROUTINE_IMPL_FOR_NAMED_ASM_TYPE.internalName, "exception", AsmTypes.JAVA_THROWABLE_TYPE.descriptor)
visitVarInsn(Opcodes.ASTORE, exceptionIndex)
invokestatic(COROUTINE_MARKER_OWNER, ACTUAL_COROUTINE_START_MARKER_NAME, "()V", false)
})
}
private fun removeUnreachableSuspensionPointsAndExitPoints(methodNode: MethodNode, suspensionPoints: MutableList<SuspensionPoint>) {
val dceResult = DeadCodeEliminationMethodTransformer().transformWithResult(containingClassInternalName, methodNode)
@@ -216,10 +317,12 @@ class CoroutineTransformerMethodVisitor(
// k + 1 - data
// k + 2 - exception
val variablesToSpill =
((exceptionIndex + 1) until localsCount)
(0 until localsCount)
.filter{ it !in setOf(continuationIndex, dataIndex, exceptionIndex) }
.map { Pair(it, frame.getLocal(it)) }
.filter { (index, value) ->
value != StrictBasicValue.UNINITIALIZED_VALUE && livenessFrame.isAlive(index)
(index == 0 && needDispatchReceiver && isForNamedFunction) ||
(value != StrictBasicValue.UNINITIALIZED_VALUE && livenessFrame.isAlive(index))
}
for ((index, basicValue) in variablesToSpill) {
@@ -446,4 +549,120 @@ private class SuspensionPoint(
}
private fun getLastParameterIndex(desc: String, access: Int) =
Type.getArgumentTypes(desc).dropLast(1).map { it.size }.sum() + (if (access and Opcodes.ACC_STATIC != 0) 0 else 1)
Type.getArgumentTypes(desc).dropLast(1).map { it.size }.sum() + (if (!isStatic(access)) 1 else 0)
private fun getParameterTypesForCoroutineConstructor(desc: String, hasDispatchReceiver: Boolean, thisName: String) =
listOfNotNull(if (!hasDispatchReceiver) null else Type.getObjectType(thisName)).toTypedArray() +
Type.getArgumentTypes(desc).last()
private fun isStatic(access: Int) = access and Opcodes.ACC_STATIC != 0
private fun getParameterTypesIndicesForCoroutineConstructor(
desc: String,
containingFunctionAccess: Int,
needDispatchReceiver: Boolean,
thisName: String
): Collection<Pair<Type, Int>> {
return mutableListOf<Pair<Type, Int>>().apply {
if (needDispatchReceiver) {
add(Type.getObjectType(thisName) to 0)
}
val continuationIndex =
getAllParameterTypes(desc, !isStatic(containingFunctionAccess), thisName).dropLast(1).map(Type::getSize).sum()
add(CONTINUATION_ASM_TYPE to continuationIndex)
}
}
private fun getAllParameterTypes(desc: String, hasDispatchReceiver: Boolean, thisName: String) =
listOfNotNull(if (!hasDispatchReceiver) null else Type.getObjectType(thisName)).toTypedArray() +
Type.getArgumentTypes(desc)
private fun allSuspensionPointsAreTailCalls(
thisName: String,
methodNode: MethodNode,
suspensionPoints: List<SuspensionPoint>
): Boolean {
val safelyReachableReturns = findSafelyReachableReturns(methodNode)
val sourceFrames = MethodTransformer.analyze(thisName, methodNode, IgnoringCopyOperationSourceInterpreter())
val instructions = methodNode.instructions
return suspensionPoints.all { suspensionPoint ->
val beginIndex = instructions.indexOf(suspensionPoint.suspensionCallBegin)
val endIndex = instructions.indexOf(suspensionPoint.suspensionCallEnd)
safelyReachableReturns[endIndex + 1]?.all { returnIndex ->
val sourceInsn =
sourceFrames[returnIndex].top().sure {
"There must be some value on stack to return"
}.insns.singleOrNull()
sourceInsn?.let(instructions::indexOf) in beginIndex..endIndex
} ?: false
}
}
private class IgnoringCopyOperationSourceInterpreter : SourceInterpreter() {
override fun copyOperation(insn: AbstractInsnNode?, value: SourceValue?) = value
}
/**
* Let's call an instruction safe if its execution is always invisible: stack modifications, branching, variable insns (invisible in debug)
*
* For some instruction `insn` define the result as following:
* - if there is a path leading to the non-safe instruction then result is `null`
* - Otherwise result contains all the reachable ARETURN indices
*
* @return indices of safely reachable returns for each instruction in the method node
*/
private fun findSafelyReachableReturns(methodNode: MethodNode): Array<Set<Int>?> {
val controlFlowGraph = ControlFlowGraph.build(methodNode)
val insns = methodNode.instructions
val reachableReturnsIndices = Array<Set<Int>?>(insns.size()) init@{ index ->
val insn = insns[index]
if (insn.opcode == Opcodes.ARETURN) {
return@init setOf(index)
}
if (!insn.isMeaningful || insn.opcode in SAFE_OPCODES || insn.isInvisibleInDebugVarInsn(methodNode) ||
InlineCodegenUtil.isInlineMarker(insn)) {
setOf()
}
else null
}
var changed: Boolean
do {
changed = false
for (index in 0 until insns.size()) {
if (insns[index].opcode == Opcodes.ARETURN) continue
val newResult =
controlFlowGraph
.getSuccessorsIndices(index).plus(index)
.map(reachableReturnsIndices::get)
.fold<Set<Int>?, Set<Int>?>(mutableSetOf<Int>()) { acc, successorsResult ->
if (acc != null && successorsResult != null) acc + successorsResult else null
}
if (newResult != reachableReturnsIndices[index]) {
reachableReturnsIndices[index] = newResult
changed = true
}
}
} while (changed)
return reachableReturnsIndices
}
private fun AbstractInsnNode?.isInvisibleInDebugVarInsn(methodNode: MethodNode): Boolean {
val insns = methodNode.instructions
val index = insns.indexOf(this)
return (this is VarInsnNode && methodNode.localVariables.none {
it.index == `var` && index in it.start.let(insns::indexOf)..it.end.let(insns::indexOf)
})
}
private val SAFE_OPCODES =
((Opcodes.DUP..Opcodes.DUP2_X2) + Opcodes.POP + Opcodes.POP2 + (Opcodes.IFEQ..Opcodes.GOTO)).toSet()
@@ -20,15 +20,14 @@ import org.jetbrains.kotlin.codegen.ExpressionCodegen
import org.jetbrains.kotlin.codegen.FunctionGenerationStrategy
import org.jetbrains.kotlin.codegen.binding.CodegenBinding
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.jvm.diagnostics.OtherOrigin
import org.jetbrains.kotlin.resolve.jvm.jvmSignature.JvmMethodSignature
import org.jetbrains.org.objectweb.asm.Label
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.jetbrains.org.objectweb.asm.MethodVisitor
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
class SuspendFunctionGenerationStrategy(
state: GenerationState,
@@ -36,98 +35,40 @@ class SuspendFunctionGenerationStrategy(
private val declaration: KtFunction,
private val containingClassInternalName: String
) : FunctionGenerationStrategy.CodegenBased(state) {
private val containsNonTailSuspensionCalls = originalSuspendDescriptor.containsNonTailSuspensionCalls(state.bindingContext)
private lateinit var transformer: CoroutineTransformerMethodVisitor
private lateinit var codegen: ExpressionCodegen
private val classBuilderForCoroutineState by lazy {
state.factory.newVisitor(
OtherOrigin(declaration, originalSuspendDescriptor),
CodegenBinding.asmTypeForAnonymousClass(state.bindingContext, originalSuspendDescriptor),
declaration.containingFile
)
).also {
val coroutineCodegen =
CoroutineCodegenForNamedFunction.create(it, codegen, originalSuspendDescriptor, declaration)
coroutineCodegen.generate()
}
}
override fun wrapMethodVisitor(mv: MethodVisitor, access: Int, name: String, desc: String): MethodVisitor {
if (containsNonTailSuspensionCalls) {
return CoroutineTransformerMethodVisitor(
mv, access, name, desc, null, null, containingClassInternalName, classBuilderForCoroutineState,
isForNamedFunction = true
)
}
if (access and Opcodes.ACC_ABSTRACT != 0) return mv
return super.wrapMethodVisitor(mv, access, name, desc)
return CoroutineTransformerMethodVisitor(
mv, access, name, desc, null, null, containingClassInternalName, this::classBuilderForCoroutineState,
isForNamedFunction = true,
needDispatchReceiver = originalSuspendDescriptor.dispatchReceiverParameter != null,
internalNameForDispatchReceiver = containingClassInternalNameOrNull()
).also {
transformer = it
}
}
private fun containingClassInternalNameOrNull() =
originalSuspendDescriptor.containingDeclaration.safeAs<ClassDescriptor>()?.let(state.typeMapper::mapClass)?.internalName
override fun doGenerateBody(codegen: ExpressionCodegen, signature: JvmMethodSignature) {
if (!containsNonTailSuspensionCalls) {
codegen.returnExpression(declaration.bodyExpression)
return
}
val coroutineCodegen =
CoroutineForNamedFunctionCodegen.create(classBuilderForCoroutineState, codegen, originalSuspendDescriptor, declaration)
coroutineCodegen.generate()
val functionDescriptor = codegen.context.functionDescriptor
val continuationIndex = codegen.frameMap.getIndex(functionDescriptor.valueParameters.last())
val objectTypeForState = Type.getObjectType(classBuilderForCoroutineState.thisName)
val dataIndex = codegen.frameMap.enterTemp(AsmTypes.OBJECT_TYPE)
val exceptionIndex = codegen.frameMap.enterTemp(AsmTypes.OBJECT_TYPE)
with(codegen.v) {
val createStateInstance = Label()
val storeStateObject = Label()
// We have to distinguish the following situations:
// - Our function got called in a common way (e.g. from another function or via recursive call) and we should execute our
// code from the beginning
// - We got called from `doResume` of our continuation, i.e. we need to continue from the last suspension point
//
// Also in the first case we wrap the completion into a special anonymous class instance (let's call it X$1)
// that we'll use as a continuation argument for suspension points
//
// How we distinguish the cases:
// - If the continuation is not an instance of X$1 we know exactly it's not the second case, because when resuming
// the continuation we pass an instance of that class
// - Otherwise it's still can be a recursive call. To check it's not the case we set the last bit in the label in
// `doResume` just before calling the suspend function (see kotlin.coroutines.experimental.jvm.internal.CoroutineImplForNamedFunction).
// So, if it's set we're in continuation.
visitVarInsn(Opcodes.ALOAD, continuationIndex)
instanceOf(objectTypeForState)
ifeq(createStateInstance)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
invokevirtual(
COROUTINE_IMPL_FOR_NAMED_ASM_TYPE.internalName,
"checkAndFlushLastBit", Type.getMethodDescriptor(Type.BOOLEAN_TYPE), false
)
ifeq(createStateInstance)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
goTo(storeStateObject)
visitLabel(createStateInstance)
coroutineCodegen.putInstanceOnStack(codegen, null).put(objectTypeForState, this)
visitLabel(storeStateObject)
visitVarInsn(Opcodes.ASTORE, continuationIndex)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
getfield(COROUTINE_IMPL_FOR_NAMED_ASM_TYPE.internalName, "data", AsmTypes.OBJECT_TYPE.descriptor)
visitVarInsn(Opcodes.ASTORE, dataIndex)
visitVarInsn(Opcodes.ALOAD, continuationIndex)
checkcast(objectTypeForState)
getfield(COROUTINE_IMPL_FOR_NAMED_ASM_TYPE.internalName, "exception", AsmTypes.JAVA_THROWABLE_TYPE.descriptor)
visitVarInsn(Opcodes.ASTORE, exceptionIndex)
invokestatic(COROUTINE_MARKER_OWNER, ACTUAL_COROUTINE_START_MARKER_NAME, "()V", false)
}
this.codegen = codegen
codegen.returnExpression(declaration.bodyExpression)
}
}
@@ -26,7 +26,6 @@ import org.jetbrains.kotlin.codegen.binding.CodegenBinding
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper
import org.jetbrains.kotlin.codegen.topLevelClassAsmType
import org.jetbrains.kotlin.codegen.topLevelClassInternalName
import org.jetbrains.kotlin.coroutines.isSuspendLambda
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.impl.ValueParameterDescriptorImpl
@@ -156,18 +155,11 @@ fun ResolvedCall<*>.replaceSuspensionFunctionWithRealDescriptor(
return ResolvedCallWithRealDescriptor(newCall, thisExpression)
}
fun ResolvedCall<*>.isSuspensionPointInStateMachine(bindingContext: BindingContext): Boolean {
if (resultingDescriptor.safeAs<FunctionDescriptor>()?.isSuspend != true) return false
val enclosingSuspendFunction = bindingContext[BindingContext.ENCLOSING_SUSPEND_FUNCTION_FOR_SUSPEND_FUNCTION_CALL, call] ?: return false
return enclosingSuspendFunction.isStateMachineNeeded(bindingContext)
}
fun FunctionDescriptor.isStateMachineNeeded(bindingContext: BindingContext) =
isSuspendLambda || containsNonTailSuspensionCalls(bindingContext)
fun FunctionDescriptor.containsNonTailSuspensionCalls(bindingContext: BindingContext) =
bindingContext[BindingContext.CONTAINS_NON_TAIL_SUSPEND_CALLS, original] == true
fun ResolvedCall<*>.isSuspendNoInlineCall() =
resultingDescriptor.safeAs<FunctionDescriptor>()
?.let {
it.isSuspend && (!it.isInline || it.isBuiltInSuspendCoroutineOrReturnInJvm())
} == true
fun CallableDescriptor.isSuspendFunctionNotSuspensionView(): Boolean {
if (this !is FunctionDescriptor) return false
@@ -27,7 +27,6 @@ import org.jetbrains.kotlin.builtins.BuiltInsPackageFragment;
import org.jetbrains.kotlin.codegen.*;
import org.jetbrains.kotlin.codegen.context.*;
import org.jetbrains.kotlin.codegen.coroutines.CoroutineCodegenUtilKt;
import org.jetbrains.kotlin.codegen.coroutines.SuspendFunctionGenerationStrategy;
import org.jetbrains.kotlin.codegen.intrinsics.IntrinsicArrayConstructorsKt;
import org.jetbrains.kotlin.codegen.state.GenerationState;
import org.jetbrains.kotlin.codegen.state.KotlinTypeMapper;
@@ -593,14 +592,6 @@ public class InlineCodegen extends CallGenerator {
else if (expression instanceof KtFunctionLiteral) {
strategy = new ClosureGenerationStrategy(state, (KtDeclarationWithBody) expression);
}
else if (descriptor.isSuspend() && expression instanceof KtFunction) {
strategy =
new SuspendFunctionGenerationStrategy(
state,
CoroutineCodegenUtilKt.unwrapInitialDescriptorForSuspendFunction(descriptor),
(KtFunction) expression, null
);
}
else {
strategy = new FunctionGenerationStrategy.FunctionDefault(state, (KtDeclarationWithBody) expression);
}
@@ -25,7 +25,8 @@ import org.jetbrains.org.objectweb.asm.tree.analysis.BasicValue
class ControlFlowGraph private constructor(private val insns: InsnList) {
private val edges: Array<MutableList<Int>> = Array(insns.size()) { arrayListOf<Int>() }
fun getSuccessorsIndices(insn: AbstractInsnNode): List<Int> = edges[insns.indexOf(insn)]
fun getSuccessorsIndices(insn: AbstractInsnNode): List<Int> = getSuccessorsIndices(insns.indexOf(insn))
fun getSuccessorsIndices(index: Int): List<Int> = edges[index]
companion object {
@JvmStatic
@@ -787,7 +787,8 @@ public class KotlinTypeMapper {
isInterfaceMember = true;
}
else {
boolean isPrivateFunInvocation = Visibilities.isPrivate(functionDescriptor.getVisibility());
boolean isPrivateFunInvocation =
Visibilities.isPrivate(functionDescriptor.getVisibility()) && !functionDescriptor.isSuspend();
invokeOpcode = superCall || isPrivateFunInvocation ? INVOKESPECIAL : INVOKEVIRTUAL;
isInterfaceMember = superCall && currentIsInterface;
}
@@ -0,0 +1,31 @@
// IGNORE_BACKEND: JS
// WITH_RUNTIME
// WITH_COROUTINES
import helpers.*
// CHECK_BYTECODE_LISTING
import kotlin.coroutines.experimental.*
import kotlin.coroutines.experimental.intrinsics.*
inline suspend fun suspendThere(v: String): String = suspendCoroutineOrReturn { x ->
x.resume(v)
COROUTINE_SUSPENDED
}
// There's no state machine in the suspendHere, since it's inline
inline suspend fun suspendHere(): String = suspendThere("O") + suspendThere("K")
// There should be a state machine for mainSuspend as it has two suspend non-tail calls inlined
suspend fun mainSuspend() = suspendHere()
fun builder(c: suspend () -> Unit) {
c.startCoroutine(EmptyContinuation)
}
fun box(): String {
var result = ""
builder {
result = mainSuspend()
}
return result
}
@@ -0,0 +1,31 @@
@kotlin.Metadata
final class InlineWithStateMachineKt$box$1 {
synthetic final field $result: kotlin.jvm.internal.Ref$ObjectRef
field L$0: java.lang.Object
inner class InlineWithStateMachineKt$box$1
method <init>(p0: kotlin.jvm.internal.Ref$ObjectRef, p1: kotlin.coroutines.experimental.Continuation): void
public final @org.jetbrains.annotations.NotNull method create(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): kotlin.coroutines.experimental.Continuation
public final @org.jetbrains.annotations.Nullable method doResume(@org.jetbrains.annotations.Nullable p0: java.lang.Object, @org.jetbrains.annotations.Nullable p1: java.lang.Throwable): java.lang.Object
public final @org.jetbrains.annotations.Nullable method invoke(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public synthetic method invoke(p0: java.lang.Object): java.lang.Object
}
@kotlin.Metadata
final class InlineWithStateMachineKt$mainSuspend$1 {
field L$0: java.lang.Object
field L$1: java.lang.Object
inner class InlineWithStateMachineKt$mainSuspend$1
method <init>(p0: kotlin.coroutines.experimental.Continuation): void
public final @org.jetbrains.annotations.Nullable method doResume(): java.lang.Object
}
@kotlin.Metadata
public final class InlineWithStateMachineKt {
inner class InlineWithStateMachineKt$box$1
inner class InlineWithStateMachineKt$mainSuspend$1
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 mainSuspend(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public final static @org.jetbrains.annotations.Nullable method suspendHere(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public final static @org.jetbrains.annotations.Nullable method suspendThere(@org.jetbrains.annotations.NotNull p0: java.lang.String, @org.jetbrains.annotations.NotNull p1: kotlin.coroutines.experimental.Continuation): java.lang.Object
}
@@ -0,0 +1,38 @@
// IGNORE_BACKEND: NATIVE
// WITH_RUNTIME
// WITH_COROUTINES
import helpers.*
// CHECK_BYTECODE_LISTING
import kotlin.coroutines.experimental.*
import kotlin.coroutines.experimental.intrinsics.*
inline suspend fun suspendThere(v: String): String = suspendCoroutineOrReturn { x ->
x.resume(v)
COROUTINE_SUSPENDED
}
suspend fun suspendHere(): String = suspendThere("O")
// There is a kind of redundant state machine generated for complexSuspend:
// it's basically has the only suspend call just before return, but there is
// a redundant CHECKCAST String in the run's lambda, so we have to insert the state machine.
// TODO: Think of avoiding such redundant casts
suspend fun complexSuspend(): String {
return run {
suspendThere("K")
}
}
fun builder(c: suspend () -> Unit) {
c.startCoroutine(EmptyContinuation)
}
fun box(): String {
var result = ""
builder {
result = suspendHere() + complexSuspend()
}
return result
}
@@ -0,0 +1,32 @@
@kotlin.Metadata
final class InlineWithoutStateMachineKt$box$1 {
synthetic final field $result: kotlin.jvm.internal.Ref$ObjectRef
field L$0: java.lang.Object
field L$1: java.lang.Object
inner class InlineWithoutStateMachineKt$box$1
method <init>(p0: kotlin.jvm.internal.Ref$ObjectRef, p1: kotlin.coroutines.experimental.Continuation): void
public final @org.jetbrains.annotations.NotNull method create(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): kotlin.coroutines.experimental.Continuation
public final @org.jetbrains.annotations.Nullable method doResume(@org.jetbrains.annotations.Nullable p0: java.lang.Object, @org.jetbrains.annotations.Nullable p1: java.lang.Throwable): java.lang.Object
public final @org.jetbrains.annotations.Nullable method invoke(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public synthetic method invoke(p0: java.lang.Object): java.lang.Object
}
@kotlin.Metadata
final class InlineWithoutStateMachineKt$complexSuspend$1 {
field L$0: java.lang.Object
field L$1: java.lang.Object
inner class InlineWithoutStateMachineKt$complexSuspend$1
method <init>(p0: kotlin.coroutines.experimental.Continuation): void
public final @org.jetbrains.annotations.Nullable method doResume(): java.lang.Object
}
@kotlin.Metadata
public final class InlineWithoutStateMachineKt {
inner class InlineWithoutStateMachineKt$box$1
inner class InlineWithoutStateMachineKt$complexSuspend$1
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 complexSuspend(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public final static @org.jetbrains.annotations.Nullable method suspendHere(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public final static @org.jetbrains.annotations.Nullable method suspendThere(@org.jetbrains.annotations.NotNull p0: java.lang.String, @org.jetbrains.annotations.NotNull p1: kotlin.coroutines.experimental.Continuation): java.lang.Object
}
@@ -0,0 +1,28 @@
// IGNORE_BACKEND: NATIVE
// WITH_RUNTIME
// WITH_COROUTINES
// CHECK_BYTECODE_LISTING
import helpers.*
import kotlin.coroutines.experimental.*
import kotlin.coroutines.experimental.intrinsics.*
suspend fun suspendThere(v: String): String = suspendCoroutineOrReturn { x ->
x.resume(v)
COROUTINE_SUSPENDED
}
suspend fun suspendHere(): String = suspendThere("OK")
fun builder(c: suspend () -> Unit) {
c.startCoroutine(EmptyContinuation)
}
fun box(): String {
var result = ""
builder {
result = suspendHere()
}
return result
}
@@ -0,0 +1,20 @@
@kotlin.Metadata
final class SimpleKt$box$1 {
synthetic final field $result: kotlin.jvm.internal.Ref$ObjectRef
field L$0: java.lang.Object
inner class SimpleKt$box$1
method <init>(p0: kotlin.jvm.internal.Ref$ObjectRef, p1: kotlin.coroutines.experimental.Continuation): void
public final @org.jetbrains.annotations.NotNull method create(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): kotlin.coroutines.experimental.Continuation
public final @org.jetbrains.annotations.Nullable method doResume(@org.jetbrains.annotations.Nullable p0: java.lang.Object, @org.jetbrains.annotations.Nullable p1: java.lang.Throwable): java.lang.Object
public final @org.jetbrains.annotations.Nullable method invoke(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public synthetic method invoke(p0: java.lang.Object): java.lang.Object
}
@kotlin.Metadata
public final class SimpleKt {
inner class SimpleKt$box$1
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 suspendHere(@org.jetbrains.annotations.NotNull p0: kotlin.coroutines.experimental.Continuation): java.lang.Object
public final static @org.jetbrains.annotations.Nullable method suspendThere(@org.jetbrains.annotations.NotNull p0: java.lang.String, @org.jetbrains.annotations.NotNull p1: kotlin.coroutines.experimental.Continuation): java.lang.Object
}
@@ -1,17 +1,19 @@
@kotlin.Metadata
final class Controller$multipleSuspensions$1 {
field L$0: java.lang.Object
synthetic final field this$0: Controller
inner class Controller$multipleSuspensions$1
method <init>(p0: Controller, p1: kotlin.coroutines.experimental.Continuation): void
public final @org.jetbrains.annotations.Nullable method doResume(@org.jetbrains.annotations.Nullable p0: java.lang.Object, @org.jetbrains.annotations.Nullable p1: java.lang.Throwable): java.lang.Object
public final @org.jetbrains.annotations.Nullable method doResume(): java.lang.Object
}
@kotlin.Metadata
final class Controller$nonTailCall$1 {
field L$0: java.lang.Object
synthetic final field this$0: Controller
inner class Controller$nonTailCall$1
method <init>(p0: Controller, p1: kotlin.coroutines.experimental.Continuation): void
public final @org.jetbrains.annotations.Nullable method doResume(@org.jetbrains.annotations.Nullable p0: java.lang.Object, @org.jetbrains.annotations.Nullable p1: java.lang.Throwable): java.lang.Object
public final @org.jetbrains.annotations.Nullable method doResume(): java.lang.Object
}
@kotlin.Metadata
@@ -28,9 +30,9 @@ public final class Controller {
@kotlin.Metadata
final class CoroutineFieldsKt$box$1 {
synthetic final field $result: kotlin.jvm.internal.Ref$ObjectRef
private field J$0: long
private field L$0: java.lang.Object
private field L$1: java.lang.Object
field J$0: long
field L$0: java.lang.Object
field L$1: java.lang.Object
private field p$: Controller
inner class CoroutineFieldsKt$box$1
method <init>(p0: kotlin.jvm.internal.Ref$ObjectRef, p1: kotlin.coroutines.experimental.Continuation): void
@@ -5708,6 +5708,33 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class TailCallOptimizations extends AbstractIrBlackBoxCodegenTest {
public void testAllFilesPresentInTailCallOptimizations() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/tailCallOptimizations"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM, true);
}
@TestMetadata("inlineWithStateMachine.kt")
public void testInlineWithStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithStateMachine.kt");
doTest(fileName);
}
@TestMetadata("inlineWithoutStateMachine.kt")
public void testInlineWithoutStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithoutStateMachine.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/simple.kt");
doTest(fileName);
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailOperations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -5708,6 +5708,33 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class TailCallOptimizations extends AbstractBlackBoxCodegenTest {
public void testAllFilesPresentInTailCallOptimizations() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/tailCallOptimizations"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM, true);
}
@TestMetadata("inlineWithStateMachine.kt")
public void testInlineWithStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithStateMachine.kt");
doTest(fileName);
}
@TestMetadata("inlineWithoutStateMachine.kt")
public void testInlineWithoutStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithoutStateMachine.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/simple.kt");
doTest(fileName);
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailOperations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -5708,6 +5708,33 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class TailCallOptimizations extends AbstractLightAnalysisModeTest {
public void testAllFilesPresentInTailCallOptimizations() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/tailCallOptimizations"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JVM, true);
}
@TestMetadata("inlineWithStateMachine.kt")
public void testInlineWithStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithStateMachine.kt");
doTest(fileName);
}
@TestMetadata("inlineWithoutStateMachine.kt")
public void testInlineWithoutStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithoutStateMachine.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/simple.kt");
doTest(fileName);
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailOperations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -6405,6 +6405,39 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class TailCallOptimizations extends AbstractJsCodegenBoxTest {
public void testAllFilesPresentInTailCallOptimizations() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/tailCallOptimizations"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JS, true);
}
@TestMetadata("inlineWithStateMachine.kt")
public void testInlineWithStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithStateMachine.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("inlineWithoutStateMachine.kt")
public void testInlineWithoutStateMachine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/inlineWithoutStateMachine.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/tailCallOptimizations/simple.kt");
doTest(fileName);
}
}
@TestMetadata("compiler/testData/codegen/box/coroutines/tailOperations")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)