docs/backend/IR_parameter_api_migration.md
It has been refactored how value parameters in IrFunction and value arguments in IrMemberAccessExpression are represented (KT-68003). The old API is deprecated and scheduled for removal somewhere around Kotlin 2.2.20 or 2.3. Here we provide a guide for compiler plugin authors how to migrate to the new API.
Note: compiler plugin API is still experimental, we change it here and there quite regularly. Most of those changes used to be simple (such as moving code around) and should be easy to apply by "finding something similar". However, this change is big and broad enough that we decided to have a short deprecation cycle and publish a dedicated guide for migration (also used internally). It also describes how to properly take care of the upcoming context parameters in your IR plugins.
The old API is implemented on top of the new one, which is the source of truth. Any modification done via either API should be reflected in the other one. Old API preserves all its previous semantics, except some rare corner cases around receiver arguments.
IrFunction:
var parameters: List<IrValueParameter> - It stores all value parameters: dispatch receiver, extension receiver, context parameters and regular parameters.
IrValueParameter.kind property.valueParameters, dispatchReceiverParameter (only setter), extensionReceiverParameter, contextReceiverParametersCount.
dispatchReceiverParameter is now only a handy util that searches parameters.nonDispatchParameters.IrValueParameter:
var kind: IrParameterKind, which can be either: DispatchReceiver, ExtensionReceiver, Context, Regular.
IrValueParameter in IR. For those we set thekind to:
IrClass.thisReceiver, IrScript.thisReceiver -> DispatchReceiver.IrValueParameters in IrScript -> Regular.var indexInOldValueParameters property.
valueParameters list.index property, but has a clearer name as to what it refers to.
index with indexInOldValueParameters at the beginning of migration.var indexInParameters: Int .
parameters list.indexInOldValueParameters, in case a function has any receiver parameter.indexInParameters and indexInOldValueParameters are automatically updated when adding/removing a parameter via either old or new API.index, the plan is to rename this property back to index.indexInOldValueParameters.indexInParameters.index - don't use.IrMemberAccessExpression
val arguments: MutableList<IrExpression?> - It stores all value arguments: dispatch receiver, extension receiver, context arguments and regular arguments.
IrFunction.parameters.call.arguments zip call.symbol.owner.parameters.getValueArgument, putValueArgument, valueArgumentsCount, extensionReceiver.
dispatchReceiver is not deprecated, but discouraged in most cases - please see its kdoc.IrFunction, there is also a new nonDispatchArguments util. Same as dispatchReceiver, it should be used with care.IrCallableReference
IrFunctionReference, IrFunctionExpression and IrPropertyReference are considered deprecated.IrRichFunctionReference and IrRichPropertyReference.UpgradeCallableReferences.) But the plan is to fully migrate to them eventually, so for now, we encourage to handle both new and old callable reference types.IrRichFunctionReference.To avoid the common bug when we have code like:
function.dispatchReceiverParameter?.process()
function.extensionReceiverParameter?.process()
function.valueParameters.forEach { it.process() }
while process expects the same order of parameters as present in ABI. Because then it should be:
function.dispatchReceiverParameter?.process()
function.valueParameters.take(function.contextReceiverCount).forEach { it.process() }
function.extensionReceiverParameter?.process()
function.valueParameters.drop(function.contextReceiverCount).forEach { it.process() }
Now it simply becomes:
function.parameters.forEach { it.process() }
We often have a code like
if (valueParameters.any { it.type.isLambda && it.isInlne }) { ... }
which is OK, because in the language, receivers cannot be inline. But if we'd remove isInline, it likely becomes a bug because extension receiver can have a functional type. OOTH we check context parameters here, which also cannot be inline.
Another example: it's easy to miss declaration.extensionReceiverParameter?.accept(this) in visitors.
We should rather process all parameters/arguments by default. That way we may also future-proof additional language features for other parameter kinds, just in case. An example is bound context parameters in callable extension, see 3.
So far only dispatch and extension receivers could be bound in a reference to function or property, but context parameters will also need to be bound. It will be easier to implement with unified parameters.
(An example branch: wlitewka/ir-parameters-migration-backend-jvm)
parameters and arguments).
IrMemberAccessExpression.dispatchReceiver also counts, but it's not strictly deprecated - please see its KDoc.IrRichCallableReference), that refactoring is not fully testable - we still only test binding dispatch receiver or extension receiver. So for now treat it as best-effort.KCallable), which will still only support binding dispatch and extension receivers. We may be able to implement binding contexts in the future.IrParameterKind when it is required.
valueParameters (or only arguments returned by getValueArgument()). This means that it sees Regular and Context parameters/arguments, and ignores DispatchReceiver or ExtensionReceiver. Rarely is it the most appropriate combination. The migrate code should instead:
Regular parameters or arguments, then, in almost all cases, it should also handle ExtensionReceiver and Context , because from the backend perspective, both of them are mostly just a synthetic sugar for rather "normal" parameters.DispatchReceiver quite often requires special handling. It should be decided case by case whether to use parameters or nonDispatchParameters. Still, parameters are preferable.function.valueParameter.any { it.isInline }
just replace valueParameters with parameters. Currently, kotlin only permits inline on regular parameters, but it should not hurt to consider all of them.IrConstructor is particularly interesting, because:
ExtensionReceiver or Context parameters. It could possibly have Context in the future, when the context parameter design expands to constructors or classes, but for now we assume those should be processed the same way as Regular.DispatchReceiver, in case it is a constructor of inner class. That parameter is then turned into Regular at some point during lowering.valueParameters with nonDispatchParameters. But in most cases it should be replaced with parameters instead.val contextArguments = (call.arguments zip call.symbol.owner.parameters).filter { it.second.kind == IrParameterKind.Context }IrCallableReference or IrProperty. In that case, use getAllArgumentsWithIr().function.dispatchReceiverParameter, function.nonDispatchParameters, call.dispatchReceiver, call.nonDispatchArguments .arguments of a call matches 1-to-1 with parameters of a callee. In other words, arguments.size == parameters.size,.
valueParameters before. But now it also extends to receiver parameters, and it wasn't always perfect. Occasionally we could have a call that had dispatchReceiver != null, while on callee dispatchReceiverParameter == null (or vice versa), at least for some time in the compilation pipeline. Think @JvmStatic, inner class's constructor, members of IrScript - tricky cases like that. It used to work somehow, but because now receivers are in the list, such inconsistency can cause index mismatch for subsequent parameters/arguments.call.dispatchReceiver == null could either mean "there is no dispatch argument" or "dispatch argument is (temporarily) null". In new API, the former is arguments = [], the latter arguments = [null].arguments:
To fill out arguments of some call, we used to have a code like this:
call.extensionReceiver = ...
call.putValueArgument(0, ...)
call.putValueArgument(1, ...)
We assumed we don't really need dispatch/extensionReceiver, because:
I.e.:
call.arguments[0] = ...
call.arguments[1] = ...
call.arguments[2] = ...
In case you have IrValueParameter at hand, you can also write call.arguments[param] instead of call.arguments[param.indexInParameter]. It would be preferred, actually.
Specifically for dispatch receiver - when you know the callee has one, it has to be the first parameter - it is OK to just hardcode arguments[0].
arguments zip parameters, or similar.
call.arguments.assignFrom(irExpressionList).IrFunction.hasShape().
val isAnyEquals =
function.name == StandardNames.EQUALS_NAME &&
function.dispatchReceiverParameter != null &&
function.extensionReceiverParameter == null &&
function.valueParameters.size == 1 &&
function.valueParameters[0].type == irBuiltIns.anyNType
into
val isEqualsOnAny =
function.name == StandardNames.EQUALS_NAME &&
function.hasShape(
dispatchReceiver = true,
regularParameters = 1,
parameterTypes = listOf(null, irBuiltIns.anyNType)
)
function.valueParamters.isEmpty()
into
function.hasShape(regularParamters = 0)
function.valueParamters.singleOrNull()?.type?.isInt() == true
into
function.hasShape(regularParamters = 1, parameterTypes = listOf(irBuiltIns.intType))
function.valueParamters.singleOrNull()?.type?.isPrimitiveType() == true
into
function.hasShape(regularParamters = 1) && function.parameters[0].type.isPrimitiveType()