Basic step-by-step inlining example for proposed model

Let's check the following code:

// lib
fun process(x: Int) { /* some code here */ }
inline fun run(block: () -> Int) = process(block())
// main
fun foo() {
    run { 42 }
}

We would dig deeper into the compilation of the main module, assuming lib is already compiled.

After frontend execution, we get something like that. Here we have all calls resolved. Functions from dependencies (lib) are loaded as LazyIr.

fun foo() : Unit {
    run(
        lambda@{ return@lambda 42 }
    )
}

// dependencies: 
// lazyIR without bodies
fun process(x: Int): Unit
inline fun run(block: Function0<Int>)
// ... lazyIR for Int, Unit, Function and so on 

Then, pre-inline lowering happens. But as we have a very simple example, there is nothing to do.

Next, we need to load Ir of run function. For that we need to run Deserializer. But we can‘t run linker, so references inside function body wouldn’t be resolved.

//  IrFunction
//      name = run
//      isInline = true
//      valueParameter0 
//        irType
//           classifier = Lazy class kotlin.Function0 (from Lazy run function)
//           typeArgument0 = Lazy class kotlin.Int (from Lazy run function)
//      returnType = Lazy class kotlin.Unit (from Lazy run function)
//      body
//           IrCall symbol = Unbound function symbol with signature "process(Int) : Unit"
//                returnType = IrType classifier = Unbound class symbol with singnature kotlin.Unit 
//                valueArgument0 = 
//                    IrCall symbol = Unbound function symbol with signature Function0.invoke() 
//                         typeArgument0 = IrType classifier = Unbound class symbol with singnature kotlin.Int
//                         valueArguemnt0 = IrGet valueParamenter block
//                         type = Unbound class symbol with singnature kotlin.Int 
//
inline fun run(block: Function0<Int>) : Unit { // note, that here we merged LazyIr of run function with deserialized body
    process(block.invoke())
}

Now this function can be inlined to original.

// IrFunction 
//   name = foo
//   returnType = Lazy class kotlin.Unit
//   body
//       IrReturnableBlock symbol=symbol1
//         type = Lazy class kotlin.Unit
//         IrInlinedFunctionBlock required for debug information
//            IrReturn 
//               target=symbol1
//               value = IrCall 
//                 symbol = Unbound function symbol with signature "process(Int) : Unit"
//                 returnType = IrType classifier = Unbound class symbol with singnature kotlin.Unit 
//                 valueArgument0 
//                    IrReturnableBlock symbol=symbol2
//                      type = Lazy class for kotlin.Int
//                      IrInlinedFunctionBlock required for debug information
//                         IrReturn target=symbol2 value = IrConst<Int>(42)  
fun foo() : Unit {
    inlinedBlock@{
        return@inlinedBlock process(lambda@ { return@lambda 42 })
    }
}

Several side notes on the result:

  1. We have both lazy references to kotlin.Unit/kotlin.Int and unbound ones in the tree now. It is fine, as we only need to deserialize it now.
  2. While inlining we need to understand that we need special handling of Unbound function symbol with signature Function0.invoke(), this must be done by signature only.
  3. IrReturnableBlock is represented like it works now, while IrInlinedFunctionBlock is significantly simplified. Probably we need to redesign both to make them serializable.