org.jetbrains.kotlin.codegen.inline.MethodInliner.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-compiler-embeddable Show documentation
Show all versions of kotlin-compiler-embeddable Show documentation
the Kotlin compiler embeddable
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.*
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_ASM_TYPE
import org.jetbrains.kotlin.codegen.inline.FieldRemapper.Companion.foldName
import org.jetbrains.kotlin.codegen.inline.coroutines.CoroutineTransformer
import org.jetbrains.kotlin.codegen.inline.coroutines.markNoinlineLambdaIfSuspend
import org.jetbrains.kotlin.codegen.inline.coroutines.surroundInvokesWithSuspendMarkersIfNeeded
import org.jetbrains.kotlin.codegen.optimization.ApiVersionCallsPreprocessingMethodTransformer
import org.jetbrains.kotlin.codegen.optimization.FixStackWithLabelNormalizationMethodTransformer
import org.jetbrains.kotlin.codegen.optimization.common.*
import org.jetbrains.kotlin.codegen.optimization.fixStack.*
import org.jetbrains.kotlin.codegen.optimization.nullCheck.isCheckParameterIsNotNull
import org.jetbrains.kotlin.codegen.optimization.temporaryVals.TemporaryVariablesEliminationTransformer
import org.jetbrains.kotlin.codegen.pseudoInsns.PseudoInsn
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.jvm.AsmTypes.OBJECT_TYPE
import org.jetbrains.kotlin.utils.SmartList
import org.jetbrains.kotlin.utils.SmartSet
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
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
import org.jetbrains.org.objectweb.asm.commons.LocalVariablesSorter
import org.jetbrains.org.objectweb.asm.commons.MethodRemapper
import org.jetbrains.org.objectweb.asm.tree.*
import org.jetbrains.org.objectweb.asm.tree.analysis.BasicValue
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
import org.jetbrains.org.objectweb.asm.util.Printer
import java.util.*
import kotlin.math.max
class MethodInliner(
private val node: MethodNode,
private val parameters: Parameters,
private val inliningContext: InliningContext,
private val nodeRemapper: FieldRemapper,
private val isSameModule: Boolean,
private val errorPrefixSupplier: () -> String,
private val sourceMapper: SourceMapCopier,
private val inlineCallSiteInfo: InlineCallSiteInfo,
private val isInlineOnlyMethod: Boolean = false,
private val shouldPreprocessApiVersionCalls: Boolean = false,
private val defaultMaskStart: Int = -1,
private val defaultMaskEnd: Int = -1
) {
private val languageVersionSettings = inliningContext.state.languageVersionSettings
private val invokeCalls = ArrayList()
//keeps order
private val transformations = ArrayList()
//current state
private val currentTypeMapping = HashMap()
private val result = InlineResult.create()
private var lambdasFinallyBlocks: Int = 0
fun doInline(
adapter: MethodVisitor,
remapper: LocalVarRemapper,
remapReturn: Boolean,
returnLabels: Map
): InlineResult {
return doInline(adapter, remapper, remapReturn, returnLabels, 0)
}
private fun recordTransformation(info: TransformationInfo) {
if (!inliningContext.isInliningLambda) {
inliningContext.root.state.globalInlineContext.recordTypeFromInlineFunction(info.oldClassName)
}
if (info.shouldRegenerate(isSameModule)) {
inliningContext.recordRegeneratedAnonymousObject(info.oldClassName)
}
transformations.add(info)
}
private fun doInline(
adapter: MethodVisitor,
remapper: LocalVarRemapper,
remapReturn: Boolean,
returnLabels: Map,
finallyDeepShift: Int
): InlineResult {
//analyze body
var transformedNode = markPlacesForInlineAndRemoveInlinable(node, returnLabels, finallyDeepShift)
//substitute returns with "goto end" instruction to keep non local returns in lambdas
val end = linkedLabel()
val isTransformingAnonymousObject = nodeRemapper is RegeneratedLambdaFieldRemapper
transformedNode = doInline(transformedNode)
if (!isTransformingAnonymousObject) {
//don't remove assertion in transformed anonymous object
removeClosureAssertions(transformedNode)
}
transformedNode.instructions.resetLabels()
val resultNode = MethodNode(
Opcodes.API_VERSION, transformedNode.access, transformedNode.name, transformedNode.desc,
transformedNode.signature, transformedNode.exceptions?.toTypedArray()
)
val visitor = RemapVisitor(resultNode, remapper, nodeRemapper)
try {
transformedNode.accept(
if (isTransformingAnonymousObject) {
/*keep annotations and attributes during anonymous object transformations*/
visitor
} else MethodBodyVisitor(visitor)
)
} catch (e: Throwable) {
throw wrapException(e, transformedNode, "couldn't inline method call")
}
resultNode.visitLabel(end)
if (inliningContext.isRoot) {
val remapValue = remapper.remap(parameters.argsSizeOnStack + 1).value
InternalFinallyBlockInliner.processInlineFunFinallyBlocks(
resultNode, lambdasFinallyBlocks, (remapValue as StackValue.Local).index,
languageVersionSettings.supportsFeature(LanguageFeature.ProperFinally)
)
}
if (remapReturn) {
processReturns(resultNode, returnLabels, end)
}
//flush transformed node to output
resultNode.accept(SkipMaxAndEndVisitor(adapter))
return result
}
private fun doInline(node: MethodNode): MethodNode {
val currentInvokes = LinkedList(invokeCalls)
val resultNode = MethodNode(node.access, node.name, node.desc, node.signature, null)
val iterator = transformations.iterator()
val remapper = TypeRemapper.createFrom(currentTypeMapping)
// MethodRemapper doesn't extends LocalVariablesSorter, but RemappingMethodAdapter does.
// So wrapping with LocalVariablesSorter to keep old behavior
// TODO: investigate LocalVariablesSorter removing (see also same code in RemappingClassBuilder.java)
val localVariablesSorter = LocalVariablesSorter(resultNode.access, resultNode.desc, wrapWithMaxLocalCalc(resultNode))
val remappingMethodAdapter = MethodRemapper(localVariablesSorter, AsmTypeRemapper(remapper, result))
val fakeContinuationName = CoroutineTransformer.findFakeContinuationConstructorClassName(node)
val markerShift = calcMarkerShift(parameters, node)
var currentLineNumber = if (isInlineOnlyMethod) sourceMapper.callSite!!.line else -1
val lambdaInliner = object : InlineAdapter(remappingMethodAdapter, parameters.argsSizeOnStack, sourceMapper) {
private var transformationInfo: TransformationInfo? = null
private var currentLabel: Label? = null
override fun visitLabel(label: Label?) {
currentLabel = label
super.visitLabel(label)
}
override fun visitLineNumber(line: Int, start: Label) {
if (!isInlineOnlyMethod) {
currentLineNumber = line
}
super.visitLineNumber(line, start)
}
private fun handleAnonymousObjectRegeneration() {
transformationInfo = iterator.next()
val oldClassName = transformationInfo!!.oldClassName
if (inliningContext.parent?.transformationInfo?.oldClassName == oldClassName) {
// Object constructs itself, don't enter an infinite recursion of regeneration.
} else if (transformationInfo!!.shouldRegenerate(isSameModule)) {
//TODO: need poping of type but what to do with local funs???
val newClassName = transformationInfo!!.newClassName
remapper.addMapping(oldClassName, newClassName)
val childInliningContext = inliningContext.subInlineWithClassRegeneration(
inliningContext.nameGenerator,
currentTypeMapping,
inlineCallSiteInfo,
transformationInfo!!
)
val transformer = transformationInfo!!.createTransformer(
childInliningContext, isSameModule, fakeContinuationName
)
val transformResult = transformer.doTransform(nodeRemapper)
transformResult.getChangedTypes().forEach { (oldType, newType) ->
// KT-65503 For all changed types, if oldType is a lambda or an anonymous object,
// and the newType is a name for an inline call,
// it should be added to the remapper to ensure correct inline conversion of nested anonymous objects or lambdas.
if (newType.contains(INLINE_CALL_TRANSFORMATION_SUFFIX) &&
!oldType.contains(INLINE_CALL_TRANSFORMATION_SUFFIX) &&
isAnonymousClass(oldType) &&
!remapper.hasNoAdditionalMapping(oldType)
) {
remapper.addMapping(oldType, newType)
}
}
result.merge(transformResult)
result.addChangedType(oldClassName, newClassName)
if (inliningContext.isInliningLambda &&
inliningContext.lambdaInfo !is DefaultLambda && //never delete default lambda classes
transformationInfo!!.canRemoveAfterTransformation() &&
!inliningContext.root.state.globalInlineContext.isTypeFromInlineFunction(oldClassName)
) {
// this class is transformed and original not used so we should remove original one after inlining
result.addClassToRemove(oldClassName)
}
if (transformResult.reifiedTypeParametersUsages.wereUsedReifiedParameters()) {
ReifiedTypeInliner.putNeedClassReificationMarker(mv)
result.reifiedTypeParametersUsages.mergeAll(transformResult.reifiedTypeParametersUsages)
}
for (classBuilder in childInliningContext.continuationBuilders.values) {
classBuilder.done(inliningContext.state.config.generateSmapCopyToAnnotation)
}
} else {
result.addNotChangedClass(oldClassName)
}
}
override fun anew(type: Type) {
if (isSamWrapper(type.internalName) || isAnonymousClass(type.internalName)) {
handleAnonymousObjectRegeneration()
}
//in case of regenerated transformationInfo type would be remapped to new one via remappingMethodAdapter
super.anew(type)
}
override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) {
if (/*INLINE_RUNTIME.equals(owner) &&*/ isInvokeOnLambda(owner, name)) { //TODO add method
assert(!currentInvokes.isEmpty())
val invokeCall = currentInvokes.remove()
val info = invokeCall.functionalArgument
if (info !is LambdaInfo) {
//noninlinable lambda
markNoinlineLambdaIfSuspend(mv, info)
super.visitMethodInsn(opcode, owner, name, desc, itf)
return
}
val nullableAnyType = inliningContext.state.module.builtIns.nullableAnyType
val expectedParameters = info.invokeMethod.argumentTypes
val expectedKotlinParameters = info.invokeMethodParameters
val argumentCount = Type.getArgumentTypes(desc).size.let {
if (info is PsiExpressionLambda && info.invokeMethodDescriptor.isSuspend && it < expectedParameters.size) {
// Inlining suspend lambda into a function that takes a non-suspend lambda.
// In the IR backend, this cannot happen as inline lambdas are not lowered.
addFakeContinuationMarker(this)
it + 1
} else it
}
assert(argumentCount == expectedParameters.size && argumentCount == expectedKotlinParameters.size) {
"inconsistent lambda arguments: $argumentCount on stack, ${expectedParameters.size} expected, " +
"${expectedKotlinParameters.size} Kotlin types"
}
var valueParamShift = max(nextLocalIndex, markerShift) + expectedParameters.sumOf { it.size }
for (index in argumentCount - 1 downTo 0) {
val type = expectedParameters[index]
StackValue.coerce(AsmTypes.OBJECT_TYPE, nullableAnyType, type, expectedKotlinParameters[index], this)
valueParamShift -= type.size
store(valueParamShift, type)
}
if (expectedParameters.isEmpty()) {
nop() // add something for a line number to bind onto (TODO what line number?)
}
val firstLine = info.node.node.instructions.asSequence().mapNotNull { it as? LineNumberNode }.firstOrNull()?.line ?: -1
if ((info is DefaultLambda != isInlineOnlyMethod) && currentLineNumber >= 0 && firstLine == currentLineNumber) {
// This can happen in two cases:
// 1. `someInlineOnlyFunction { singleLineLambda }`: in this case line numbers are removed
// from the inline function, so the entirety of its bytecode has the line number of
// the call site;
// 2. `inline fun someFunction(defaultLambda: ... = { ... }) = something(defaultLambda())`:
// all of `someFunction`, including `defaultLambda` if no value is provided at call site,
// has the line number of the declaration.
// In those cases the debugger is unable to observe the boundary between the body of the function
// and the inline lambda call, as they have the exact same line number. So to force a JDI
// event we insert a fake line number separating those two real stretches. The event corresponding
// to the fake line number itself should be ignored by the debugger though.
val label = Label()
val fakeLineNumber =
sourceMapper.parent.mapSyntheticLineNumber(SourceMapper.LOCAL_VARIABLE_INLINE_ARGUMENT_SYNTHETIC_LINE_NUMBER)
mv.visitLabel(label)
mv.visitLineNumber(fakeLineNumber, label)
}
addInlineMarker(this, true)
val lambdaParameters = info.addAllParameters(nodeRemapper)
val newCapturedRemapper = InlinedLambdaRemapper(
info.lambdaClassType.internalName, nodeRemapper, lambdaParameters,
info is DefaultLambda && info.isBoundCallableReference
)
setLambdaInlining(true)
val callSite = sourceMapper.callSite.takeIf { info is DefaultLambda }
val inliner = MethodInliner(
info.node.node, lambdaParameters, inliningContext.subInlineLambda(info),
newCapturedRemapper,
if (info is DefaultLambda) isSameModule else true /*cause all nested objects in same module as lambda*/,
{ "Lambda inlining " + info.lambdaClassType.internalName },
SourceMapCopier(sourceMapper.parent, info.node.classSMAP, callSite), inlineCallSiteInfo,
isInlineOnlyMethod = false
)
val varRemapper = LocalVarRemapper(lambdaParameters, valueParamShift)
val inlineScopesGenerator = inliningContext.inlineScopesGenerator
val label = currentLabel
// When regenerating anonymous objects we may inline a crossinline lambda before some
// already inlined functions. For these functions their scope numbers should be incremented.
// We also need to temporarily increment the already inlined scopes number by the number of
// inline marker variables that we have found before the crossinline lambda call to assign
// the scope number for this lambda correctly.
val inlineScopeNumberIncrement =
if (inlineScopesGenerator != null && label != null && isRegeneratingAnonymousObject()) {
incrementScopeNumbersOfVariables(node, label)
} else {
0
}
inlineScopesGenerator?.apply {
inlinedScopes += inlineScopeNumberIncrement
currentCallSiteLineNumber =
if (isInlineOnlyMethod) {
currentLineNumber
} else {
sourceMapper.mapLineNumber(currentLineNumber)
}
}
//TODO add skipped this and receiver
val lambdaResult =
inliner.doInline(localVariablesSorter, varRemapper, true, info.returnLabels, invokeCall.finallyDepthShift)
inlineScopesGenerator?.apply { inlinedScopes -= inlineScopeNumberIncrement }
result.mergeWithNotChangeInfo(lambdaResult)
result.reifiedTypeParametersUsages.mergeAll(lambdaResult.reifiedTypeParametersUsages)
result.reifiedTypeParametersUsages.mergeAll(info.reifiedTypeParametersUsages)
StackValue.coerce(info.invokeMethod.returnType, info.invokeMethodReturnType, OBJECT_TYPE, nullableAnyType, this)
setLambdaInlining(false)
addInlineMarker(this, false)
if (currentLineNumber != -1) {
val endLabel = Label()
mv.visitLabel(endLabel)
if (isInlineOnlyMethod) {
// This is from the function we're inlining into, so no need to remap.
mv.visitLineNumber(currentLineNumber, endLabel)
} else {
// Need to go through the superclass here to properly remap the line number via `sourceMapper`.
super.visitLineNumber(currentLineNumber, endLabel)
}
}
} else if (isAnonymousConstructorCall(owner, name)) { //TODO add method
//TODO add proper message
val newInfo = transformationInfo as? AnonymousObjectTransformationInfo ?: throw AssertionError(
" call doesn't correspond to object transformation info for '$owner.$name': $transformationInfo"
)
// inline fun f() -> new f$1 -> fun something() in class f$1 -> new f$1
// ^-- fetch the info that was created for this instruction
// Currently, this self-reference pattern only happens in coroutine objects, which construct
// copies of themselves in `create` or `invoke` (not `invokeSuspend`).
val existingInfo = inliningContext.parent?.transformationInfo?.takeIf { it.oldClassName == newInfo.oldClassName }
as AnonymousObjectTransformationInfo?
val info = existingInfo ?: newInfo
if (info.shouldRegenerate(isSameModule)) {
for (capturedParamDesc in info.allRecapturedParameters) {
val realDesc = if (existingInfo != null && capturedParamDesc.fieldName == AsmUtil.THIS) {
// The captures in `info` are relative to the parent context, so a normal `this` there
// is a captured outer `this` here.
CapturedParamDesc(Type.getObjectType(owner), AsmUtil.CAPTURED_THIS_FIELD, capturedParamDesc.type)
} else capturedParamDesc
visitFieldInsn(
Opcodes.GETSTATIC, realDesc.containingLambdaName,
foldName(realDesc.fieldName), realDesc.type.descriptor
)
}
super.visitMethodInsn(opcode, info.newClassName, name, info.newConstructorDescriptor, itf)
//TODO: add new inner class also for other contexts
if (inliningContext.parent is RegeneratedClassContext) {
inliningContext.parent.typeRemapper.addAdditionalMappings(info.oldClassName, info.newClassName)
}
transformationInfo = null
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf)
}
} else if (ReifiedTypeInliner.isNeedClassReificationMarker(MethodInsnNode(opcode, owner, name, desc, false))) {
// If objects are reified, the marker will be recreated by `handleAnonymousObjectRegeneration` above.
if (!inliningContext.shouldReifyTypeParametersInObjects) {
super.visitMethodInsn(opcode, owner, name, desc, itf)
}
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf)
}
}
override fun visitFieldInsn(opcode: Int, owner: String, name: String, desc: String) {
if (opcode == Opcodes.GETSTATIC && (isAnonymousSingletonLoad(owner, name) || isWhenMappingAccess(owner, name))) {
handleAnonymousObjectRegeneration()
}
super.visitFieldInsn(opcode, owner, name, desc)
}
override fun visitMaxs(stack: Int, locals: Int) {
lambdasFinallyBlocks = resultNode.tryCatchBlocks.size
super.visitMaxs(stack, locals)
}
}
node.accept(lambdaInliner)
surroundInvokesWithSuspendMarkersIfNeeded(resultNode)
if (inliningContext.inlineScopesGenerator != null && GENERATE_SMAP) {
updateCallSiteLineNumbers(resultNode, node)
}
return resultNode
}
private fun updateCallSiteLineNumbers(resultNode: MethodNode, inlinedNode: MethodNode) {
val inlinedNodeLocalVariables = inlinedNode.localVariables ?: return
val resultNodeLocalVariables = resultNode.localVariables ?: return
if (inlinedNodeLocalVariables.isEmpty() || resultNodeLocalVariables.isEmpty()) {
return
}
val markerVariablesFromInlinedNode = inlinedNodeLocalVariables.filter { isFakeLocalVariableForInline(it.name) }
if (markerVariablesFromInlinedNode.isEmpty()) {
return
}
val markerVariableNamesFromInlinedNode = markerVariablesFromInlinedNode.map { it.name }.toMutableSet()
// When updating the call site line numbers, we need to skip the marker variable of the inlined node - it has
// already been assigned a correct call site line number during inlining. However, when regenerating anonymous objects,
// the inliner copies the bodies of the regenerated methods and no marker variables are introduced during this process.
// So in case with anonymous object regeneration we don't have to skip anything.
if (!isRegeneratingAnonymousObject()) {
val labelToIndex = inlinedNode.getLabelToIndexMap()
val markerVariableOfInlinedNode = markerVariablesFromInlinedNode.sortedBy { labelToIndex[it.start.label] }.first()
markerVariableNamesFromInlinedNode.remove(markerVariableOfInlinedNode.name)
}
for (variable in resultNodeLocalVariables) {
val name = variable.name
if (isFakeLocalVariableForInline(name) && name in markerVariableNamesFromInlinedNode) {
variable.name = updateCallSiteLineNumber(name) { sourceMapper.mapLineNumber(it) }
}
}
}
private fun prepareNode(node: MethodNode, finallyDeepShift: Int): MethodNode {
node.instructions.resetLabels()
val capturedParamsSize = parameters.capturedParametersSizeOnStack
val realParametersSize = parameters.realParametersSizeOnStack
val reorderIrLambdaParameters = inliningContext.isInliningLambda &&
inliningContext.parent?.isInliningLambda == false &&
inliningContext.lambdaInfo is IrExpressionLambda
val oldArgumentTypes = if (reorderIrLambdaParameters) {
// In IR lambdas, captured variables come before real parameters, but after the extension receiver.
// Move them to the end of the descriptor instead.
Type.getArgumentTypes(inliningContext.lambdaInfo!!.invokeMethod.descriptor)
} else {
Type.getArgumentTypes(node.desc)
}
val oldArgumentOffsets = oldArgumentTypes.runningFold(0) { acc, type -> acc + type.size }
val newArgumentTypes = oldArgumentTypes.filterIndexed { index, _ ->
oldArgumentOffsets[index] !in defaultMaskStart..defaultMaskEnd
}.toTypedArray() + parameters.capturedTypes
val transformedNode = MethodNode(
Opcodes.API_VERSION, node.access, node.name,
Type.getMethodDescriptor(Type.getReturnType(node.desc), *newArgumentTypes),
node.signature, node.exceptions?.toTypedArray()
)
inliningContext.inlineScopesGenerator?.addInlineScopesInfo(node, isRegeneratingAnonymousObject())
val transformationVisitor = object : InlineMethodInstructionAdapter(transformedNode) {
private val GENERATE_DEBUG_INFO = GENERATE_SMAP && !isInlineOnlyMethod
private val isInliningLambda = nodeRemapper.isInsideInliningLambda
private fun getNewIndex(`var`: Int): Int {
val lambdaInfo = inliningContext.lambdaInfo
@Suppress("USELESS_IS_CHECK") // K2 warning suppression, TODO: KT-62472
if (reorderIrLambdaParameters && lambdaInfo is IrExpressionLambda) {
val extensionSize = if (lambdaInfo.isExtensionLambda) lambdaInfo.invokeMethod.argumentTypes[0].size else 0
return when {
// v-- extensionSize v-- argsSizeOnStack
// |- extension -|- captured -|- real -|- locals -| old descriptor
// |- extension -|- real -|- captured -|- locals -| new descriptor
// ^-- realParametersSize
`var` >= parameters.argsSizeOnStack -> `var`
`var` >= extensionSize + capturedParamsSize -> `var` - capturedParamsSize
`var` >= extensionSize -> `var` + realParametersSize - extensionSize
else -> `var`
}
}
// |- extension -|- real -|- locals -| old descriptor
// |- extension -|- real -|- captured -|- locals -| new descriptor
return `var` + if (`var` < realParametersSize) 0 else capturedParamsSize
}
override fun visitVarInsn(opcode: Int, `var`: Int) {
super.visitVarInsn(opcode, getNewIndex(`var`))
}
override fun visitIincInsn(`var`: Int, increment: Int) {
super.visitIincInsn(getNewIndex(`var`), increment)
}
override fun visitMaxs(maxStack: Int, maxLocals: Int) {
super.visitMaxs(maxStack, maxLocals + capturedParamsSize)
}
override fun visitLineNumber(line: Int, start: Label) {
if (isInliningLambda || GENERATE_DEBUG_INFO) {
super.visitLineNumber(line, start)
}
}
override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) {
if (DEFAULT_LAMBDA_FAKE_CALL == owner) {
val index = name.substringAfter(DEFAULT_LAMBDA_FAKE_CALL).toInt()
val lambda = getFunctionalArgumentIfExists(index) as DefaultLambda
for (captured in lambda.capturedVars.asReversed()) {
lambda.originalBoundReceiverType?.let {
// The receiver is the only captured value; it needs to be boxed.
StackValue.onStack(it).put(captured.type, InstructionAdapter(this))
}
super.visitFieldInsn(
Opcodes.PUTSTATIC,
captured.containingLambdaName,
CAPTURED_FIELD_FOLD_PREFIX + captured.fieldName,
captured.type.descriptor
)
}
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf)
}
}
override fun visitLocalVariable(name: String, desc: String, signature: String?, start: Label, end: Label, index: Int) {
if (!isInliningLambda && !GENERATE_DEBUG_INFO) return
val isInlineFunctionMarker = name.startsWith(JvmAbi.LOCAL_VARIABLE_NAME_PREFIX_INLINE_FUNCTION)
val newName = when {
inliningContext.isRoot && !isInlineFunctionMarker -> {
if (inliningContext.inlineScopesGenerator != null) {
calculateNewNameUsingScopeNumbers(name)
} else {
calculateNewNameUsingTheOldScheme(name)
}
}
else -> name
}
super.visitLocalVariable(newName, desc, signature, start, end, getNewIndex(index))
}
private fun calculateNewNameUsingScopeNumbers(name: String): String {
if (name.startsWith(AsmUtil.THIS)) {
val scopeNumber = name.getInlineScopeInfo()?.scopeNumber ?: return AsmUtil.INLINE_DECLARATION_SITE_THIS
return "${AsmUtil.INLINE_DECLARATION_SITE_THIS}$INLINE_SCOPE_NUMBER_SEPARATOR$scopeNumber"
}
return name
}
private fun calculateNewNameUsingTheOldScheme(name: String): String {
val namePrefix = if (name == AsmUtil.THIS) AsmUtil.INLINE_DECLARATION_SITE_THIS else name
return namePrefix + INLINE_FUN_VAR_SUFFIX
}
}
node.accept(transformationVisitor)
transformCaptured(transformedNode)
transformFinallyDeepIndex(transformedNode, finallyDeepShift)
return transformedNode
}
private fun markPlacesForInlineAndRemoveInlinable(
node: MethodNode, returnLabels: Map, finallyDeepShift: Int
): MethodNode {
val processingNode = prepareNode(node, finallyDeepShift)
preprocessNodeBeforeInline(processingNode, returnLabels)
replaceContinuationAccessesWithFakeContinuationsIfNeeded(processingNode)
val sources = analyzeMethodNodeWithInterpreter(processingNode, FunctionalArgumentInterpreter(this))
val instructions = processingNode.instructions
val toDelete = markObsoleteInstruction(instructions, sources)
var awaitClassReification = false
var currentFinallyDeep = 0
InsnSequence(instructions).forEach { cur ->
val frame = sources[instructions.indexOf(cur)]
if (frame != null) {
when {
ReifiedTypeInliner.isNeedClassReificationMarker(cur) -> awaitClassReification = true
cur is MethodInsnNode -> {
if (isFinallyStart(cur)) {
//TODO deep index calc could be more precise
currentFinallyDeep = getConstant(cur.previous)
}
val owner = cur.owner
val name = cur.name
//TODO check closure
val argTypes = Type.getArgumentTypes(cur.desc)
val paramCount = argTypes.size + 1//non static
val firstParameterIndex = frame.stackSize - paramCount
if (isInvokeOnLambda(owner, name) /*&& methodInsnNode.owner.equals(INLINE_RUNTIME)*/) {
val sourceValue = frame.getStack(firstParameterIndex)
invokeCalls.add(InvokeCall(sourceValue.functionalArgument, currentFinallyDeep))
} else if (isSamWrapperConstructorCall(owner, name)) {
recordTransformation(SamWrapperTransformationInfo(owner, inliningContext, isAlreadyRegenerated(owner)))
} else if (isAnonymousConstructorCall(owner, name)) {
val functionalArgumentMapping = HashMap()
var offset = 0
var capturesAnonymousObjectThatMustBeRegenerated = false
for (i in 0 until paramCount) {
val sourceValue = frame.getStack(firstParameterIndex + i)
val functionalArgument = sourceValue.functionalArgument
if (functionalArgument != null) {
functionalArgumentMapping[offset] = functionalArgument
} else if (i < argTypes.size && isAnonymousClassThatMustBeRegenerated(argTypes[i])) {
capturesAnonymousObjectThatMustBeRegenerated = true
}
offset += if (i == 0) 1 else argTypes[i - 1].size
}
recordTransformation(
buildConstructorInvocation(
owner,
cur.desc,
functionalArgumentMapping,
awaitClassReification,
capturesAnonymousObjectThatMustBeRegenerated
)
)
awaitClassReification = false
} else if (inliningContext.isInliningLambda && ReifiedTypeInliner.isOperationReifiedMarker(cur)) {
val reificationArgument = cur.reificationArgument
val parameterName = reificationArgument!!.parameterName
result.reifiedTypeParametersUsages.addUsedReifiedParameter(parameterName)
}
}
cur.opcode == Opcodes.GETSTATIC -> {
val fieldInsnNode = cur as FieldInsnNode?
val className = fieldInsnNode!!.owner
when {
isAnonymousSingletonLoad(className, fieldInsnNode.name) -> {
recordTransformation(
AnonymousObjectTransformationInfo(
className, awaitClassReification, isAlreadyRegenerated(className), true,
inliningContext.nameGenerator
)
)
awaitClassReification = false
}
isWhenMappingAccess(className, fieldInsnNode.name) -> {
recordTransformation(
WhenMappingTransformationInfo(
className, inliningContext.nameGenerator, isAlreadyRegenerated(className), fieldInsnNode
)
)
}
fieldInsnNode.isCheckAssertionsStatus() -> {
fieldInsnNode.owner = inlineCallSiteInfo.ownerClassName
when {
// In inline function itself:
inliningContext.parent == null -> inliningContext
// In method of regenerated object - field should already exist:
inliningContext.parent is RegeneratedClassContext -> inliningContext.parent
// In lambda inlined into the root function:
inliningContext.parent.parent == null -> inliningContext.parent
// In lambda inlined into a method of a regenerated object:
else -> inliningContext.parent.parent as? RegeneratedClassContext
?: throw AssertionError("couldn't find class for \$assertionsDisabled (context = $inliningContext)")
}.generateAssertField = true
}
}
}
cur.opcode == Opcodes.POP -> {
if (frame.top().functionalArgument is LambdaInfo) {
toDelete.add(cur)
}
}
cur.opcode == Opcodes.PUTFIELD -> {
//Recognize next contract's pattern in inline lambda
// ALOAD 0
// SOME_VALUE
// PUTFIELD $capturedField
// and transform it to
// SOME_VALUE
// PUTSTATIC $$$$capturedField
val fieldInsn = cur as FieldInsnNode
if (isCapturedFieldName(fieldInsn.name) &&
nodeRemapper is InlinedLambdaRemapper &&
nodeRemapper.originalLambdaInternalName == fieldInsn.owner
) {
val stackTransformations = mutableSetOf()
val lambdaInfo = frame.peek(1)?.functionalArgument
if (lambdaInfo is LambdaInfo && stackTransformations.all { it is VarInsnNode }) {
assert(lambdaInfo.lambdaClassType.internalName == nodeRemapper.originalLambdaInternalName) {
"Wrong bytecode template for contract template: ${lambdaInfo.lambdaClassType.internalName} != ${nodeRemapper.originalLambdaInternalName}"
}
fieldInsn.name = foldName(fieldInsn.name)
fieldInsn.opcode = Opcodes.PUTSTATIC
toDelete.addAll(stackTransformations)
}
}
}
}
} else {
//given frame is null if and only if the corresponding instruction cannot be reached (dead code).
//clean dead code otherwise there is problems in unreachable finally block, don't touch label it cause try/catch/finally problems
if (cur.nodeType == AbstractInsnNode.LABEL) {
//NB: Cause we generate exception table for default handler using gaps (see ExpressionCodegen.visitTryExpression)
//it may occurs that interval for default handler starts before catch start label, so this label seems as dead,
//but as result all this labels will be merged into one (see KT-5863)
} else {
toDelete.add(cur)
}
}
}
processingNode.remove(toDelete)
//clean dead try/catch blocks
processingNode.tryCatchBlocks.removeIf { it.isMeaningless() }
return processingNode
}
private fun markObsoleteInstruction(instructions: InsnList, sources: Array?>): SmartSet {
return instructions.filterIndexedTo(SmartSet.create()) { index, insn ->
// Parameter checks are processed separately
!insn.isAloadBeforeCheckParameterIsNotNull() && when (insn.opcode) {
Opcodes.GETFIELD, Opcodes.GETSTATIC, Opcodes.ALOAD ->
sources[index + 1]?.top().functionalArgument is LambdaInfo
Opcodes.PUTFIELD, Opcodes.PUTSTATIC, Opcodes.ASTORE ->
sources[index]?.top().functionalArgument is LambdaInfo
Opcodes.SWAP ->
sources[index]?.peek(0).functionalArgument is LambdaInfo || sources[index]?.peek(1).functionalArgument is LambdaInfo
else -> false
}
}
}
// Replace ALOAD 0
// with
// ICONST fakeContinuationMarker
// INVOKESTATIC InlineMarker.mark
// ACONST_NULL
// iff this ALOAD 0 is continuation and one of the following conditions is met
// 1) it is passed as the last parameter to suspending function
// 2) it is ASTORE'd right after
// 3) it is passed to invoke of lambda
private fun replaceContinuationAccessesWithFakeContinuationsIfNeeded(processingNode: MethodNode) {
// in ir backend inline suspend lambdas do not use ALOAD 0 to get continuation, since they are generated as static functions
// instead they get continuation from parameter.
val lambdaInfo = inliningContext.lambdaInfo ?: return
if (lambdaInfo !is PsiExpressionLambda || !lambdaInfo.invokeMethodDescriptor.isSuspend) return
val sources = analyzeMethodNodeWithInterpreter(processingNode, Aload0Interpreter(processingNode))
val cfg = ControlFlowGraph.build(processingNode)
val aload0s = processingNode.instructions.asSequence().filter { it.opcode == Opcodes.ALOAD && (it as? VarInsnNode)?.`var` == 0 }
val visited = hashSetOf()
fun findMeaningfulSuccs(insn: AbstractInsnNode): Collection {
if (!visited.add(insn)) return emptySet()
val res = hashSetOf()
for (succIndex in cfg.getSuccessorsIndices(insn)) {
val succ = processingNode.instructions[succIndex]
if (succ.isMeaningful) res.add(succ)
else res.addAll(findMeaningfulSuccs(succ))
}
return res
}
// After inlining suspendCoroutineUninterceptedOrReturn there will be suspension point, which is not a MethodInsnNode.
// So, it is incorrect to expect MethodInsnNodes only
val suspensionPoints = processingNode.instructions.asSequence()
.filter { isBeforeSuspendMarker(it) }
.flatMap { findMeaningfulSuccs(it).asSequence() }
.filter { it is MethodInsnNode }
val toReplace = hashSetOf()
for (suspensionPoint in suspensionPoints) {
assert(suspensionPoint is MethodInsnNode) {
"suspensionPoint shall be MethodInsnNode, but instead $suspensionPoint"
}
suspensionPoint as MethodInsnNode
assert(Type.getReturnType(suspensionPoint.desc) == OBJECT_TYPE) {
"suspensionPoint shall return $OBJECT_TYPE, but returns ${Type.getReturnType(suspensionPoint.desc)}"
}
val frame = sources[processingNode.instructions.indexOf(suspensionPoint)] ?: continue
val paramTypes = Type.getArgumentTypes(suspensionPoint.desc)
if (suspensionPoint.name.endsWith(JvmAbi.DEFAULT_PARAMS_IMPL_SUFFIX)) {
// Expected pattern here:
// ALOAD 0
// (ICONST or other integers creating instruction)
// (ACONST_NULL or ALOAD)
// ICONST_0
// INVOKESTATIC InlineMarker.mark
// INVOKE* suspendingFunction$default(..., Continuation;ILjava/lang/Object)Ljava/lang/Object;
assert(paramTypes.size >= 3) {
"${suspensionPoint.name}${suspensionPoint.desc} shall have 3+ parameters"
}
} else {
// Expected pattern here:
// ALOAD 0
// ICONST_0
// INVOKESTATIC InlineMarker.mark
// INVOKE* suspendingFunction(..., Continuation;)Ljava/lang/Object;
assert(paramTypes.isNotEmpty()) {
"${suspensionPoint.name}${suspensionPoint.desc} shall have 1+ parameters"
}
}
for ((index, param) in paramTypes.reversed().withIndex()) {
if (param != CONTINUATION_ASM_TYPE && param != OBJECT_TYPE) continue
val sourceIndices = (frame.getStack(frame.stackSize - index - 1) as? Aload0BasicValue)?.indices ?: continue
for (sourceIndex in sourceIndices) {
val src = processingNode.instructions[sourceIndex]
if (src in aload0s) {
toReplace.add(src)
}
}
}
}
// Expected pattern here:
// ALOAD 0
// ASTORE N
// This pattern may occur after multiple inlines
// Note, that this is not a suspension point, thus we check it separately
toReplace.addAll(aload0s.filter { it.next?.opcode == Opcodes.ASTORE })
// Expected pattern here:
// ALOAD 0
// INVOKEINTERFACE kotlin/jvm/functions/FunctionN.invoke (...,Ljava/lang/Object;)Ljava/lang/Object;
toReplace.addAll(aload0s.filter { isLambdaCall(it.next) })
replaceContinuationsWithFakeOnes(toReplace, processingNode)
}
private fun isLambdaCall(invoke: AbstractInsnNode?): Boolean {
if (invoke?.opcode != Opcodes.INVOKEINTERFACE) return false
invoke as MethodInsnNode
if (!invoke.owner.startsWith("kotlin/jvm/functions/Function")) return false
if (invoke.name != "invoke") return false
if (Type.getReturnType(invoke.desc) != OBJECT_TYPE) return false
return Type.getArgumentTypes(invoke.desc).let { it.isNotEmpty() && it.last() == OBJECT_TYPE }
}
private fun replaceContinuationsWithFakeOnes(
continuations: Collection,
node: MethodNode
) {
for (toReplace in continuations) {
insertNodeBefore(createFakeContinuationMethodNodeForInline(), node, toReplace)
node.instructions.remove(toReplace)
}
}
private fun preprocessNodeBeforeInline(node: MethodNode, returnLabels: Map) {
try {
InplaceArgumentsMethodTransformer().transform("fake", node)
FixStackWithLabelNormalizationMethodTransformer().transform("fake", node)
TemporaryVariablesEliminationTransformer(inliningContext.state).transform("fake", node)
} catch (e: Throwable) {
throw wrapException(e, node, "couldn't inline method call")
}
if (shouldPreprocessApiVersionCalls) {
val targetApiVersion = inliningContext.state.languageVersionSettings.apiVersion
ApiVersionCallsPreprocessingMethodTransformer(targetApiVersion).transform("fake", node)
}
removeFakeVariablesInitializationIfPresent(node)
val analyzer = FastStackAnalyzer("", node, FixStackInterpreter()) { nLocals, nStack -> Frame(nLocals, nStack) }
val frames = analyzer.analyze()
val localReturnsNormalizer = LocalReturnsNormalizer()
for ((index, insnNode) in node.instructions.toArray().withIndex()) {
val frame = frames[index] ?: continue
// Don't care about dead code, it will be eliminated
if (!isReturnOpcode(insnNode.opcode)) continue
// TODO extract isLocalReturn / isNonLocalReturn, see processReturns
val labelName = getMarkedReturnLabelOrNull(insnNode)
if (labelName == null) {
localReturnsNormalizer.addLocalReturnToTransform(insnNode, insnNode, frame)
} else if (labelName in returnLabels) {
localReturnsNormalizer.addLocalReturnToTransform(insnNode, insnNode.previous, frame)
}
}
localReturnsNormalizer.transform(node)
}
private fun removeFakeVariablesInitializationIfPresent(node: MethodNode) {
// Before 1.6, we generated fake variable initialization instructions
// ICONST_0
// ISTORE x
// for all inline functions. Original intent was to mark inline function body for the debugger with corresponding LVT entry.
// However, for @InlineOnly functions corresponding LVT entries were not copied (assuming that nobody is actually debugging
// @InlineOnly functions).
// Since 1.6, we no longer generate fake variables for @InlineOnly functions
// Here we erase fake variable initialization for @InlineOnly functions inlined into existing bytecode (e.g., inline function
// inside third-party library).
// We consider a sequence of instructions 'ICONST_0; ISTORE x' a fake variable initialization if the corresponding variable 'x'
// is not used in the bytecode (see below).
val insnArray = node.instructions.toArray()
// Very conservative variable usage check.
// Here we look at integer variables only (this includes integral primitive types: byte, char, short, boolean).
// Variable is considered "used" if:
// - it's loaded with ILOAD instruction
// - it's incremented with IINC instruction
// - there's a local variable table entry for this variable
val usedIntegerVar = BooleanArray(node.maxLocals)
for (insn in insnArray) {
if (insn.nodeType == AbstractInsnNode.VAR_INSN && insn.opcode == Opcodes.ILOAD) {
usedIntegerVar[(insn as VarInsnNode).`var`] = true
} else if (insn.nodeType == AbstractInsnNode.IINC_INSN) {
usedIntegerVar[(insn as IincInsnNode).`var`] = true
}
}
for (localVariable in node.localVariables) {
val d0 = localVariable.desc[0]
// byte || char || short || int || boolean
if (d0 == 'B' || d0 == 'C' || d0 == 'S' || d0 == 'I' || d0 == 'Z') {
usedIntegerVar[localVariable.index] = true
}
}
// Looking for sequences of instructions:
// p0: ICONST_0
// p1: ISTORE x
// p2: