Do not transform state-machine when inlining

Sometimes, state-machine, generated in inline functions with
crossinline parameter, is transformed, since all usages should be
renamed.
However, this is wrong: in this case, we will have state-machine
inside state-machine.
This fix addresses the issue.

 #KT-25893 Fixed
This commit is contained in:
Ilmir Usmanov
2018-10-09 22:42:28 +03:00
parent d3908aeb2e
commit 0934db8fbd
9 changed files with 317 additions and 142 deletions
@@ -9,8 +9,8 @@ import com.intellij.util.ArrayUtil
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.ClassBuilder
import org.jetbrains.kotlin.codegen.StackValue
import org.jetbrains.kotlin.codegen.coroutines.*
import org.jetbrains.kotlin.codegen.optimization.common.asSequence
import org.jetbrains.kotlin.codegen.coroutines.isCoroutineSuperClass
import org.jetbrains.kotlin.codegen.inline.coroutines.CoroutineTransformer
import org.jetbrains.kotlin.codegen.serialization.JvmCodegenStringTable
import org.jetbrains.kotlin.codegen.writeKotlinMetadata
import org.jetbrains.kotlin.load.java.JvmAnnotationNames
@@ -21,10 +21,8 @@ import org.jetbrains.kotlin.metadata.jvm.JvmProtoBuf
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
import org.jetbrains.kotlin.metadata.jvm.serialization.JvmStringTable
import org.jetbrains.kotlin.protobuf.MessageLite
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin.Companion.NO_ORIGIN
import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.jetbrains.org.objectweb.asm.*
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
import org.jetbrains.org.objectweb.asm.tree.*
@@ -52,6 +50,7 @@ class AnonymousObjectTransformer(
val classBuilder = createRemappingClassBuilderViaFactory(inliningContext)
val methodsToTransform = ArrayList<MethodNode>()
val metadataReader = ReadKotlinClassHeaderAnnotationVisitor()
lateinit var superClassName: String
createClassReader().accept(object : ClassVisitor(API, classBuilder.visitor) {
override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String, interfaces: Array<String>) {
@@ -59,6 +58,7 @@ class AnonymousObjectTransformer(
if (languageVersionSettings.isCoroutineSuperClass(superName)) {
inliningContext.isContinuation = true
}
superClassName = superName
}
override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) {
@@ -133,44 +133,23 @@ class AnonymousObjectTransformer(
constructor!!, allCapturedParamBuilder, constructorParamBuilder,transformationInfo, parentRemapper
)
val crossinlineSuspendElement = capturedCrossinlineSuspendElement()
val capturesCrossinlineSuspend = (!inliningContext.isInliningLambda || inliningContext.isContinuation) &&
crossinlineSuspendElement != null
val deferringMethods = ArrayList<DeferredMethodVisitor>()
generateConstructorAndFields(classBuilder, allCapturedParamBuilder, constructorParamBuilder, parentRemapper, additionalFakeParams)
val isLambdaAlreadyGeneratedAndNotGoingToBeInlined = transformationInfo.oldClassName.contains("\$\$special\$\$inlined")
val hasLambdasToInline =
((parentRemapper is RegeneratedLambdaFieldRemapper) && parentRemapper.recapturedLambdas.isNotEmpty()) || transformationInfo.capturedLambdasToInline.isNotEmpty()
val coroutineTransformer = CoroutineTransformer(
inliningContext,
classBuilder,
sourceInfo,
methodsToTransform,
superClassName
)
for (next in methodsToTransform) {
// Generate state machine for
// 1) doResume method of suspend lambda
// 2) Suspend named function
// Iff it captures crossinline suspend lambda
val generateStateMachineForLambda =
languageVersionSettings.isResumeImplMethodName(next.name) && capturesCrossinlineSuspend && inliningContext.isContinuation &&
!isLambdaAlreadyGeneratedAndNotGoingToBeInlined && hasLambdasToInline
val continuationClassName = findFakeContinuationConstructorClassName(next)
val generateStateMachineForNamedFunction =
capturesCrossinlineSuspend && !inliningContext.isContinuation && continuationClassName != null
val deferringVisitor =
if (crossinlineSuspendElement != null) {
when {
generateStateMachineForLambda -> newStateMachineForLambda(classBuilder, next, crossinlineSuspendElement)
generateStateMachineForNamedFunction -> newStateMachineForNamedFunction(
classBuilder,
next,
continuationClassName!!,
crossinlineSuspendElement
)
else -> newMethod(classBuilder, next)
}
} else newMethod(classBuilder, next)
when {
coroutineTransformer.shouldTransform(next) -> coroutineTransformer.newMethod(next)
else -> newMethod(classBuilder, next)
}
val funResult = inlineMethodAndUpdateGlobalResult(parentRemapper, deferringVisitor, next, allCapturedParamBuilder, false)
val returnType = Type.getReturnType(next.desc)
@@ -185,29 +164,9 @@ class AnonymousObjectTransformer(
}
deferringMethods.forEach { method ->
replaceFakeContinuationsWithRealOnes(
method.intermediate,
if (!inliningContext.isContinuation)
getLastParameterIndex(method.intermediate.desc, method.intermediate.access)
else 0
)
coroutineTransformer.replaceFakesWithReals(method.intermediate)
removeFinallyMarkers(method.intermediate)
method.visitEnd()
// During regeneration of named suspend functions, which capture crossinline suspend lambda, we need to spill the variables
// into continuation object.
// In order to do this, we reuse class builder, which regenerates continuation object.
if (capturesCrossinlineSuspend &&
!inliningContext.isContinuation &&
inliningContext is RegeneratedClassContext
) {
val continuationClassName = findFakeContinuationConstructorClassName(method.intermediate)
if (continuationClassName != null) {
inliningContext.continuationBuilders
.remove(continuationClassName)
?.let(ClassBuilder::done)
}
}
}
SourceMapper.flushToClassBuilder(sourceMapper, classBuilder)
@@ -226,8 +185,7 @@ class AnonymousObjectTransformer(
writeOuterInfo(visitor)
if (continuationClassName == transformationInfo.oldClassName) {
assert(inliningContext.parent?.parent is RegeneratedClassContext)
(inliningContext.parent?.parent as RegeneratedClassContext).continuationBuilders[continuationClassName] = classBuilder
coroutineTransformer.registerClassBuilder(continuationClassName)
} else {
classBuilder.done()
}
@@ -235,10 +193,6 @@ class AnonymousObjectTransformer(
return transformationResult
}
private fun capturedCrossinlineSuspendElement(): KtExpression? = inliningContext.expressionMap.values.find { lambda ->
lambda is PsiExpressionLambda && lambda.isCrossInline && lambda.invokeMethodDescriptor.isSuspend
}?.cast<PsiExpressionLambda>()?.functionWithBodyOrCallableReference
private fun writeTransformedMetadata(header: KotlinClassHeader, classBuilder: ClassBuilder) {
writeKotlinMetadata(classBuilder, state, header.kind, header.extraInt) action@ { av ->
val (newProto, newStringTable) = transformMetadata(header) ?: run {
@@ -447,63 +401,6 @@ class AnonymousObjectTransformer(
}
}
private fun newStateMachineForLambda(builder: ClassBuilder, original: MethodNode, element: KtExpression): DeferredMethodVisitor {
return DeferredMethodVisitor(
MethodNode(
original.access, original.name, original.desc, original.signature,
ArrayUtil.toStringArray(original.exceptions)
)
) {
CoroutineTransformerMethodVisitor(
builder.newMethod(
NO_ORIGIN, original.access, original.name, original.desc, original.signature,
ArrayUtil.toStringArray(original.exceptions)
), original.access, original.name, original.desc, null, null,
obtainClassBuilderForCoroutineState = { builder },
element = element,
diagnostics = state.diagnostics,
languageVersionSettings = languageVersionSettings,
shouldPreserveClassInitialization = state.constructorCallNormalizationMode.shouldPreserveClassInitialization,
containingClassInternalName = builder.thisName,
isForNamedFunction = false,
sourceFile = sourceInfo ?: "",
isCrossinlineLambda = inliningContext.isContinuation
)
}
}
private fun newStateMachineForNamedFunction(
builder: ClassBuilder,
original: MethodNode,
continuationClassName: String,
element: KtExpression
): DeferredMethodVisitor {
assert(inliningContext is RegeneratedClassContext)
return DeferredMethodVisitor(
MethodNode(
original.access, original.name, original.desc, original.signature,
ArrayUtil.toStringArray(original.exceptions)
)
) {
CoroutineTransformerMethodVisitor(
builder.newMethod(
NO_ORIGIN, original.access, original.name, original.desc, original.signature,
ArrayUtil.toStringArray(original.exceptions)
), original.access, original.name, original.desc, null, null,
obtainClassBuilderForCoroutineState = { (inliningContext as RegeneratedClassContext).continuationBuilders[continuationClassName]!! },
element = element,
diagnostics = state.diagnostics,
languageVersionSettings = languageVersionSettings,
shouldPreserveClassInitialization = state.constructorCallNormalizationMode.shouldPreserveClassInitialization,
containingClassInternalName = builder.thisName,
isForNamedFunction = true,
needDispatchReceiver = true,
internalNameForDispatchReceiver = builder.thisName,
sourceFile = sourceInfo ?: ""
)
}
}
private fun extractParametersMappingAndPatchConstructor(
constructor: MethodNode,
capturedParamBuilder: ParametersBuilder,
@@ -649,10 +546,3 @@ class AnonymousObjectTransformer(
private fun isFirstDeclSiteLambdaFieldRemapper(parentRemapper: FieldRemapper): Boolean =
parentRemapper !is RegeneratedLambdaFieldRemapper && parentRemapper !is InlinedLambdaRemapper
}
internal fun findFakeContinuationConstructorClassName(node: MethodNode): String? {
val marker = node.instructions.asSequence().firstOrNull(::isBeforeFakeContinuationConstructorCallMarker) ?: return null
val new = marker.next
assert(new?.opcode == Opcodes.NEW)
return (new as TypeInsnNode).desc
}
@@ -1,24 +1,12 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.codegen.inline
import org.jetbrains.kotlin.codegen.ClassBuilder
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.psi.KtElement
class RootInliningContext(
expressionMap: Map<Int, LambdaInfo>,
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.codegen.StackValue
import org.jetbrains.kotlin.codegen.coroutines.continuationAsmType
import org.jetbrains.kotlin.codegen.coroutines.getOrCreateJvmSuspendFunctionView
import org.jetbrains.kotlin.codegen.inline.FieldRemapper.Companion.foldName
import org.jetbrains.kotlin.codegen.inline.coroutines.CoroutineTransformer
import org.jetbrains.kotlin.codegen.intrinsics.IntrinsicMethods
import org.jetbrains.kotlin.codegen.optimization.ApiVersionCallsPreprocessingMethodTransformer
import org.jetbrains.kotlin.codegen.optimization.FixStackWithLabelNormalizationMethodTransformer
@@ -182,7 +183,7 @@ class MethodInliner(
val transformer = transformationInfo!!.createTransformer(
childInliningContext,
isSameModule,
findFakeContinuationConstructorClassName(node)
CoroutineTransformer.findFakeContinuationConstructorClassName(node)
)
val transformResult = transformer.doTransform(nodeRemapper)
@@ -0,0 +1,164 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.codegen.inline.coroutines
import com.intellij.util.ArrayUtil
import org.jetbrains.kotlin.codegen.ClassBuilder
import org.jetbrains.kotlin.codegen.coroutines.*
import org.jetbrains.kotlin.codegen.inline.*
import org.jetbrains.kotlin.codegen.optimization.common.asSequence
import org.jetbrains.kotlin.config.isReleaseCoroutines
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin
import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.jetbrains.kotlin.utils.sure
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode
import org.jetbrains.org.objectweb.asm.tree.TypeInsnNode
class CoroutineTransformer(
private val inliningContext: InliningContext,
private val classBuilder: ClassBuilder,
private val sourceFile: String?,
private val methods: List<MethodNode>,
private val superClassName: String
) {
private val state = inliningContext.state
fun shouldTransform(node: MethodNode): Boolean {
if (isContinuationNotLambda()) return false
val crossinlineSuspend = crossinlineSuspend() ?: return false
if (inliningContext.isInliningLambda && !inliningContext.isContinuation) return false
return when {
isSuspendFunction(node) -> true
isSuspendLambda(node) -> {
if (isStateMachine(node)) return false
val functionDescriptor =
crossinlineSuspend.invokeMethodDescriptor.containingDeclaration as? FunctionDescriptor ?: return true
!functionDescriptor.isInline
}
else -> false
}
}
private fun isContinuationNotLambda(): Boolean = inliningContext.isContinuation &&
if (state.languageVersionSettings.isReleaseCoroutines()) superClassName.endsWith("ContinuationImpl")
else methods.any { it.name == "getLabel" }
private fun crossinlineSuspend(): PsiExpressionLambda? = inliningContext.expressionMap.values.find {
it is PsiExpressionLambda && it.isCrossInline && it.invokeMethodDescriptor.isSuspend
}?.cast()
private fun isStateMachine(node: MethodNode): Boolean =
node.instructions.asSequence().any { it.opcode == Opcodes.INVOKESTATIC && (it as MethodInsnNode).name == "getCOROUTINE_SUSPENDED" }
private fun isSuspendLambda(node: MethodNode) = isResumeImpl(node)
fun newMethod(node: MethodNode): DeferredMethodVisitor {
val element = crossinlineSuspend()?.functionWithBodyOrCallableReference.sure {
"crossinline lambda should have element"
}
return when {
isResumeImpl(node) -> {
assert(!isStateMachine(node)) {
"Inlining/transforming state-machine"
}
newStateMachineForLambda(node, element)
}
isSuspendFunction(node) -> newStateMachineForNamedFunction(node, element)
else -> error("no need to generate state maching for ${node.name}")
}
}
private fun isResumeImpl(node: MethodNode): Boolean =
state.languageVersionSettings.isResumeImplMethodName(node.name) &&
inliningContext.isContinuation
private fun isSuspendFunction(node: MethodNode): Boolean = findFakeContinuationConstructorClassName(node) != null
private fun newStateMachineForLambda(node: MethodNode, element: KtElement): DeferredMethodVisitor {
return DeferredMethodVisitor(
MethodNode(
node.access, node.name, node.desc, node.signature,
ArrayUtil.toStringArray(node.exceptions)
)
) {
CoroutineTransformerMethodVisitor(
classBuilder.newMethod(
JvmDeclarationOrigin.NO_ORIGIN,
node.access,
node.name,
node.desc,
node.signature,
ArrayUtil.toStringArray(node.exceptions)
), node.access, node.name, node.desc, null, null,
obtainClassBuilderForCoroutineState = { classBuilder },
element = element,
diagnostics = state.diagnostics,
languageVersionSettings = state.languageVersionSettings,
shouldPreserveClassInitialization = state.constructorCallNormalizationMode.shouldPreserveClassInitialization,
containingClassInternalName = classBuilder.thisName,
isForNamedFunction = false,
sourceFile = sourceFile ?: "",
isCrossinlineLambda = inliningContext.isContinuation
)
}
}
private fun newStateMachineForNamedFunction(node: MethodNode, element: KtElement): DeferredMethodVisitor {
val continuationClassName = findFakeContinuationConstructorClassName(node)
assert(inliningContext is RegeneratedClassContext)
return DeferredMethodVisitor(
MethodNode(
node.access, node.name, node.desc, node.signature,
ArrayUtil.toStringArray(node.exceptions)
)
) {
CoroutineTransformerMethodVisitor(
classBuilder.newMethod(
JvmDeclarationOrigin.NO_ORIGIN, node.access, node.name, node.desc, node.signature,
ArrayUtil.toStringArray(node.exceptions)
), node.access, node.name, node.desc, null, null,
obtainClassBuilderForCoroutineState = { (inliningContext as RegeneratedClassContext).continuationBuilders[continuationClassName]!! },
element = element,
diagnostics = state.diagnostics,
languageVersionSettings = state.languageVersionSettings,
shouldPreserveClassInitialization = state.constructorCallNormalizationMode.shouldPreserveClassInitialization,
containingClassInternalName = classBuilder.thisName,
isForNamedFunction = true,
needDispatchReceiver = true,
internalNameForDispatchReceiver = classBuilder.thisName,
sourceFile = sourceFile ?: ""
)
}
}
fun replaceFakesWithReals(node: MethodNode) {
findFakeContinuationConstructorClassName(node)?.let(::unregisterClassBuilder)?.let(ClassBuilder::done)
replaceFakeContinuationsWithRealOnes(
node, if (!inliningContext.isContinuation) getLastParameterIndex(node.desc, node.access) else 0
)
}
fun registerClassBuilder(continuationClassName: String) {
val context = inliningContext.parent?.parent as? RegeneratedClassContext ?: error("incorrect context")
context.continuationBuilders[continuationClassName] = classBuilder
}
fun unregisterClassBuilder(continuationClassName: String) =
(inliningContext as RegeneratedClassContext).continuationBuilders.remove(continuationClassName)
companion object {
fun findFakeContinuationConstructorClassName(node: MethodNode): String? {
val marker = node.instructions.asSequence().firstOrNull(::isBeforeFakeContinuationConstructorCallMarker) ?: return null
val new = marker.next
assert(new?.opcode == Opcodes.NEW)
return (new as TypeInsnNode).desc
}
}
}
@@ -0,0 +1,32 @@
// WITH_RUNTIME
// LANGUAGE_VERSION: 1.2
// IGNORE_BACKEND: JVM
// WITH_COROUTINES
import helpers.*
import kotlin.coroutines.experimental.*
import kotlin.coroutines.experimental.intrinsics.*
fun builder(c: suspend () -> Unit) {
c.startCoroutine(EmptyContinuation)
}
interface SuspendRunnable {
suspend fun run(): String
}
inline fun inlineMe(crossinline c: suspend () -> String) = object : SuspendRunnable {
override suspend fun run() = c()
}
fun box(): String {
var res = "FAIL"
builder {
res = inlineMe { "OK" }.run()
}
return res
}
// Test for continuation of 'run' transformation.
// Since continuation is not suspend lambda, it should not have state-machine.
// @CrossinlineSuspendContinuation_1_2Kt$box$1$doResume$$inlined$inlineMe$1$1.class:
// 0 TABLESWITCH
@@ -0,0 +1,31 @@
// WITH_RUNTIME
// LANGUAGE_VERSION: 1.3
// WITH_COROUTINES
import helpers.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
fun builder(c: suspend () -> Unit) {
c.startCoroutine(EmptyContinuation)
}
interface SuspendRunnable {
suspend fun run(): String
}
inline fun inlineMe(crossinline c: suspend () -> String) = object : SuspendRunnable {
override suspend fun run() = c()
}
fun box(): String {
var res = "FAIL"
builder {
res = inlineMe { "OK" }.run()
}
return res
}
// Test for continuation of 'run' transformation.
// Since continuation is not suspend lambda, it should not have state-machine
// @CrossinlineSuspendContinuation_1_3Kt$box$1$invokeSuspend$$inlined$inlineMe$1$1.class:
// 0 TABLESWITCH
@@ -0,0 +1,13 @@
class Handler(val func: suspend (Any) -> Unit)
inline fun createHandler(crossinline handler: suspend (Any) -> Unit): Handler {
return Handler({ handler.invoke(it) })
}
fun main(args: Array<String>) {
createHandler({
if (it !is String) {}
})
}
// 2 TABLESWITCH
@@ -0,0 +1,23 @@
fun <T> builder(c: suspend () -> T): T = TODO()
class Test {
fun doWork() {
builder {
execute {
getData { getSomeString() }
}
}
}
private inline fun execute(crossinline action: suspend () -> Unit) {
builder { action() }
}
private suspend fun <T> getData(dataProvider: suspend () -> T): T = builder { dataProvider() }
private suspend fun getSomeString(): String {
return "OK"
}
}
// 4 TABLESWITCH
@@ -1090,6 +1090,16 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/bytecodeText/coroutines"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("crossinlineSuspendContinuation_1_2.kt")
public void testCrossinlineSuspendContinuation_1_2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/coroutines/crossinlineSuspendContinuation_1_2.kt");
}
@TestMetadata("crossinlineSuspendContinuation_1_3.kt")
public void testCrossinlineSuspendContinuation_1_3() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/coroutines/crossinlineSuspendContinuation_1_3.kt");
}
@TestMetadata("doNotReassignContinuation.kt")
public void testDoNotReassignContinuation() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/coroutines/doNotReassignContinuation.kt");
@@ -1297,6 +1307,29 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
runTestWithPackageReplacement("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInVarStore.kt", "kotlin.coroutines");
}
}
@TestMetadata("compiler/testData/codegen/bytecodeText/coroutines/stateMachine")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class StateMachine extends AbstractBytecodeTextTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInStateMachine() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/bytecodeText/coroutines/stateMachine"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("kt25893.kt")
public void testKt25893() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/coroutines/stateMachine/kt25893.kt");
}
@TestMetadata("withTypeParameter.kt")
public void testWithTypeParameter() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/coroutines/stateMachine/withTypeParameter.kt");
}
}
}
@TestMetadata("compiler/testData/codegen/bytecodeText/deadCodeElimination")