Back to Compose Multiplatform

New IR parameter API + migration guide

docs/backend/IR_parameter_api_migration.md

2.3.2014.2 KB
Original Source

New IR parameter API + migration guide

Summary

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.

API change

  • IrFunction:
    • Added property var parameters: List<IrValueParameter> - It stores all value parameters: dispatch receiver, extension receiver, context parameters and regular parameters.
      • You can tell the kind of a parameter with new IrValueParameter.kind property.
    • Deprecated other parameter-related APIs: valueParameters, dispatchReceiverParameter (only setter), extensionReceiverParameter, contextReceiverParametersCount.
      • The getter of dispatchReceiverParameter is now only a handy util that searches parameters.
      • There is also a new util for the opposite result: nonDispatchParameters.
      • (Because it's very common in backend to only need to handle dispatch or all-except-dispatch.)
  • IrValueParameter:
    • Added property var kind: IrParameterKind, which can be either: DispatchReceiver, ExtensionReceiver, Context, Regular.
      • It is designed to distinguish parameters of functions. But there also are a few more occurrences of IrValueParameter in IR. For those we set thekind to:
        • IrClass.thisReceiver, IrScript.thisReceiver -> DispatchReceiver.
        • Other IrValueParameters in IrScript -> Regular.
    • Added and immediately deprecated var indexInOldValueParameters property.
      • It is an index in the deprecated valueParameters list.
      • It is always equal to the deprecated index property, but has a clearer name as to what it refers to.
        • It may be helpful to replace all usages of index with indexInOldValueParameters at the beginning of migration.
    • Added property var indexInParameters: Int .
      • It is an index in parameters list.
      • It may therefore be different than indexInOldValueParameters, in case a function has any receiver parameter.
      • BothindexInParameters and indexInOldValueParameters are automatically updated when adding/removing a parameter via either old or new API.
      • Once we are able to remove the old index, the plan is to rename this property back to index.
    • This means that for now we have three indices. In summary:
      • Old API expects indexInOldValueParameters.
      • New API expects indexInParameters.
      • index - don't use.
  • IrMemberAccessExpression
    • Added property val arguments: MutableList<IrExpression?> - It stores all value arguments: dispatch receiver, extension receiver, context arguments and regular arguments.
      • It should correspond 1 to 1 with IrFunction.parameters.
      • A single list means that now you cannot tell the kind of the argument (dispatch/extension/regular) by the call-site itself. If you need this information, you have to reach out to the corresponding parameter, e.g. via usual call.arguments zip call.symbol.owner.parameters.
    • Deprecated other argument-related APIs: getValueArgument, putValueArgument, valueArgumentsCount, extensionReceiver.
      • dispatchReceiver is not deprecated, but discouraged in most cases - please see its kdoc.
      • In analogy to 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.
    • They are going to be replaced by IrRichFunctionReference and IrRichPropertyReference.
    • However, as of kotlin 2.2.0, not all compiler code was migrated to the new "rich" references yet. More importantly, the new reference types will not appear in the compiler plugin code yet. (Currently, there is a transformation from old to new ones later in the compilation process, see 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.
    • The difference between those two reference types is a bit more complex, please see the kdoc of IrRichFunctionReference.

Motivation

1. Ensure proper order in the presence of context parameters

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() }

2. Avoid not taking receiver parameters into account by accident

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.

3. Support callable references with bound context parameters

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.

What and how to migrate

(An example branch: wlitewka/ir-parameters-migration-backend-jvm)

  1. Simply replace usages of the deprecated API with the new one (i.e. mostly with parameters and arguments).
    • Note: as mentioned IrMemberAccessExpression.dispatchReceiver also counts, but it's not strictly deprecated - please see its KDoc.
  2. When possible, refactor code handling callable references to support any parameter as either bound or unbound.
    • Currently, only dispatch receiver and extension receiver can be bound.
    • With context parameters feature, context parameters will always be bound when obtaining a callable reference.
    • Instead of relying on those two constraints, we want to liberally allow all kinds of parameters to be either bound or unbound in the backend, even if the language is not able to represent it (yet).
      • Because, as of writing this, binding context parameters is not implemented yet (it requires 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.
    • The exception is kotlin reflection (mainly KCallable), which will still only support binding dispatch and extension receivers. We may be able to implement binding contexts in the future.
  3. Try to uniformly handle all kinds of parameters/arguments, even if the previous code didn't. Only filter out a specific IrParameterKind when it is required.
    • Most likely for historical reasons, the current code oftentimes only processes 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:
      • If it is interested in 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.
      • However, DispatchReceiver quite often requires special handling. It should be decided case by case whether to use parameters or nonDispatchParameters. Still, parameters are preferable.
    • Example: given code like 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.
    • Note, IrConstructor is particularly interesting, because:
      • It can never have 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.
      • It may have DispatchReceiver, in case it is a constructor of inner class. That parameter is then turned into Regular at some point during lowering.
      • Therefore, it's always safe to replace valueParameters with nonDispatchParameters. But in most cases it should be replaced with parameters instead.
  4. If you need to check a kind of an argument, you have to reach out to the corresponding parameter.
    • Example: val contextArguments = (call.arguments zip call.symbol.owner.parameters).filter { it.second.kind == IrParameterKind.Context }
    • The above won't work if you have IrCallableReference or IrProperty. In that case, use getAllArgumentsWithIr().
    • There are also shorthands specifically for dispatch receiver: function.dispatchReceiverParameter, function.nonDispatchParameters, call.dispatchReceiver, call.nonDispatchArguments .
  5. Ensure that arguments of a call matches 1-to-1 with parameters of a callee. In other words, arguments.size == parameters.size,.
    • We of course used to uphold that for 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.
    • One peculiarity is that in old API, 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].
  6. About indexing 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:

      • When you know exactly what you're calling (e.g. some builtin) - then you should know its ABI shape, you can just hardcode all indices.
        • 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].

      • When the callee is unknown - then in most cases you likely don't care what kind of argument it has. If you do, you have to arguments zip parameters, or similar.
        • In particular, when changing kinds of parameters (e.g. dispatch to regular, extension to regular), you don't need to adjust calls anymore, so long as indices stay the same.
      • Tip: you also copy/assign arguments with call.arguments.assignFrom(irExpressionList).
  7. Quite often parameters are used in a check for a particular known function (usually from stdlib), or otherwise some known function shape. In those cases, instead of filtering specific parameters, please try out IrFunction.hasShape().
    • Examples:
      • 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()