Add documentation on planned inline changes.

^KT-64570
This commit is contained in:
Pavel Kunyavskiy
2024-01-03 19:08:11 +01:00
committed by Space Team
parent 09713bb89e
commit 01c16ed736
10 changed files with 861 additions and 0 deletions
@@ -0,0 +1,67 @@
# Accessing private declarations from inline functions
Sometimes, inline functions have more priveliges, than their callsite has.
```kotlin
class A {
private fun bar() { println("bar")}
inline internal fun foo() = bar()
}
fun main() {
A().foo()
}
```
This code is compilable, and effectively equivalent to calling `bar()` in main.
But bar() is a private function and can't be called in main().
### Current non-jvm approach
As inlining happens after klib linking, there is no issue with that.
### Current jvm-approach
For methods, synthetic accessors are generated.
For example, class `A` from above is generated to
```
public final class A {
public A();
private final void bar();
public final void foo$main();
public static final void access$bar(A);
}
```
And this `access$bar(A)` method is called instead of `bar` inside inline functions.
Accessing classes is more complex. They are just accessed as is. Which works in simple cases, but for example
```kotlin
// other.kt
package other
class A {
private class B public constructor() {
fun bar() { println("bar")}
}
private fun produce() : Any = B()
private fun consume(b: B) { b.bar() }
inline internal fun foo() {
val x = produce() as B
consume(x)
}
}
// main.kt
fun main() {
other.A().foo()
}
```
fails with IllegalAccessError.
It is unclear if it should be considered as a bug, and deprecated, or it is intended behaviour.
Probably, we can use this approach in all backends and move it to fir2ir phase.
@@ -0,0 +1,50 @@
# Anonymous objects in inline functions.
There are two different cases of interaction between inline functions and anonymous objects.
## Anonymous object inside inline function
```kotlin
inline fun <reified T> foo(crossinline block: () -> Unit) {
val simple = object {}
val complex = object {
fun foo() = block()
}
val anotherComplex = object {
fun foo() : T? = null
}
}
fun callSite1() {
foo<Int> { println("1") }
}
fun callSite2() {
foo<String> { println("2") }
}
```
Here, we can create one class for `simple` object, but must create a class per call-site
for `complex` and `anotherComplex`. Language semantics allows us simple objects on different call-sites
be both same and different.
JVM makes this single class as an optimization, if both functions defined in one module.
Other backends always copy classes in such a case.
## Anonymous object inside lambda passed to inline function
```kotlin
inline fun <T> runTwice(block: () -> T) : Pair<T, T> {
return block() to block()
}
fun main() {
val x = runTwice {
object {
fun run() { }
}::class
}
require(x.first == x.second)
}
```
In that case, language semantics require us to have a single class.
@@ -0,0 +1,28 @@
# Calling inline functions from java
Non-suspend inline functions without reified parameters can be called from java.
This is one of the places where described evolution semantics is not conformed
So original example, when called from java
```kotlin
// dependency-v1:
inline fun depFun() = "lib.v1"
// dependency-v2
inline fun depFun() = "lib.v2"
// lib: depends on dependency-v1
fun libFun() = depFun()
// Main.java: depends on lib and dependency-v2
```
```java
public class Main {
public static void main(String[] args) {
System.out.println(libFun());
}
}
```
would now print `lib.v2` opposed to `liv.v1` in kotlin.
We plan just to ignore it.
@@ -0,0 +1,22 @@
# Inline functions can refer unavailable declarations
```kotlin
// libA
fun fooA() = 5
// libB: implementation depends on libA
inline fun fooB() = fooA()
// libC: implementation depends on libB, but not libA
fun fooC() = fooB()
```
There is a problem, while inlining `fooB` to `fooC`.
It contains call to `fooA`, but `fooA` doesn't exist for `fooC` compilation.
For jvm it is not a problem, as it would inline `invokestatic libAKt.foo` as is, without
trying to understand what does it mean.
For current non-jvm it is not a problem, as it has all transitive dependencies at inlining time.
Unfortunately, if trying to inline on compile time over IR it would be the same problem as in klibs.
And even worse, as jvm compilation doesn't (and can't) have IrLinker to fix this symbols later.
+56
View File
@@ -0,0 +1,56 @@
# Overriding with inline function
While inline function can't be open, it can override function from superclass.
```kotlin
// lib
interface I {
fun foo()
}
class A : I {
override inline fun foo() {
println("lib.v1") // changed to lib.v2 in v2
}
}
// depends on lib.v1
fun test(x: A) {
x.foo() // print("lib.v1") as it is inlined
(x as A).foo() // println("lib.v2") as it can't be inlined
}
// main depends on lib.v2
fun main() {
test(A())
}
```
This leads to inconsistency on which version would be called in which cases.
This code already emits a warning (or error if there is reified type paramter),
so we are fine with this behaviour.
On the other side, there is a trickier case with the same effect, which doesn't emit any warnings.
Probably, it is a bug, and this should be deprecated ([KT-63928](https://youtrack.jetbrains.com/issue/KT-63928)).
```kotlin
interface Foo {
fun <T> foo()
}
open class Bar {
inline fun <reified T> foo() {
println(typeOf<T>())
}
}
class Bas: Foo, Bar()
fun main() {
Bas().foo<String>() // runtime crash
}
```
This leads to a restriction: inline functions should persist as normal ones after inlining, if they can be called.
And probably, ones, that can't be called should still exist with throw exception as body.
@@ -0,0 +1,46 @@
# Why is JVM model simpler than klib?
Inline function have several more dimensions of declaration changes, compared to normal ones.
- Type parameter can be converted between reified and not
- Lambda paramerets can be inline/noinline/crossinline
- inline keyword itself can be added/removed
In klib compatibility model, we need to answer what happens in all these cases.
In jvm compatibility mode, there are effectively no inline functions on link-time
(except corner cases like [calling from java](calling-from-java.md) and [inline override](inline-override.md)).
, but in that cases, inline functions already behaves as normal ones.
So they can't be changed, and this makes mental model much easier.
For example, what this non-local return even mean, when `l` is `noinline`?
```kotlin
// MODULE: lib
// FILE: lib.kt
// version: v1
inline fun foo(l: () -> Unit) {
l()
}
// version: v2
inline fun foo(noinline l: () -> Unit) {
l()
}
// MODULE: other
// FILE: other.kt
// compile("lib:v1")
fun test() {
foo {
return
}
}
// MODULE: app
// compile("other")
// compile("lib:v2")
fun main() {
test() // Will it link?
}
```
+24
View File
@@ -0,0 +1,24 @@
### Inner classes
The basic problematic example looks like this:
```kotlin
class A(val x: Int) {
inner class B {
inline fun foo() = x
}
}
fun main() {
A(5).B().foo()
}
```
The problem is caused by the fact that there is no field, which stores A object inside B doesn't exist after
Fir2IR. This field is added by lowering (which is now exectued before inlining, but probably should be moved after),
and is a private one. This leads to two problems
1. This field doesn't exist in klib
2. Even if it exists, it shouldn't have a public signature, so can't be referenced from other klib.
There is a possibility that we can deprecate accessing this of outer class from public inline functions.
This would make fixing things a bit easier here.
@@ -0,0 +1,95 @@
# Basic step-by-step inlining example for proposed model
Let's check the following code:
```kotlin
// 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.
```kotlin
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.
```kotlin
// 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.
```kotlin
// 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.
+57
View File
@@ -0,0 +1,57 @@
# Interaction of inlining with typeOf function
[typeOf](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/type-of.html) function is a standard
library function, which gets type argument, and returns KType, corresponding with this type argument.
typeOf function is quite special for inliner, as it has reified type parameter, but is an intrinsic,
not something, which can be normally inlined
Basic case of interaction looks like the following:
```kotlin
import kotlin.reflect.*
inline fun <reified T> typeOfValue(x: T) = typeOf<T>()
fun main() {
println(typeOfValue(1)) // prints int
println(typeOfValue("a")) // prints java.lang.String
println(typeOfValue(if (true) listOf(1) else mutableListOf(null))) // prints java.util.List<java.lang.Integer?>
}
```
In interaction with inline functions call-chains it can become trickier
```kotlin
inline fun <K, reified V> typeOfMap() = typeOf<Map<K, V>>()
fun main() {
println(typeOfMap<Int, Int>()) // prints java.util.Map<K, java.lang.Integer>
}
```
In that case, `K` is neither erased nor substituted.
This is now handled by doing part of typeOf processing before inlining, as there is no K in callsite context.
On the other side, we can't process `V`, because we don't know it's value yet, so another part must be done after inlining.
For example, for code above, it would be transformed to following intermediate state before inlining
```kotlin
inline fun <K, V> typeOfMap() = KType(classifier = Map::class, typeArguemnts = [KTypeArgument(K), typeOf<V>()])
fun main() {
println(typeOfMap<Int, Int>()) // prints java.util.Map<K, java.lang.Integer>
}
```
Unfortunately, this doesn't cover all cases correctly. This means, that typeOf handling should be somehow embedded into inlining.
```kotlin
import kotlin.reflect.*
inline fun <reified T> typeOfValue(x: T) = typeOf<T>()
inline fun <T> typeOfNonReifiedList(x: List<T>) = typeOfValue(x)
fun main() {
println(typeOfNonReifiedList(listOf(1, 2, 3))) // prints java.util.List<kotlin.Int> on native, but should print java.util.List<T>
}
```
+416
View File
@@ -0,0 +1,416 @@
# Design Doc: Klib Inlining
Youtrack issue: [KT-64570](https://youtrack.jetbrains.com/issue/KT-64570)
## Problem statement
Current behavior of `inline` functions evolution diverge between JVM and non-JVM backends.
Basic example with different behavior looks like the following:
```kotlin
// dependency-v1:
inline fun depFun() = "lib.v1"
// dependency-v2
inline fun depFun() = "lib.v2"
// lib: depends on dependency-v1
fun libFun() = depFun()
// main: depends on lib and dependency-v2
fun main() {
println(libFun())
}
```
On jvm this code would print `lib.v1`, as code is already inlined in libFun bytecode, and
it would not change independently of actual available function code.
On the other side, on klib-based backends this code would print `lib.v2`, as inline function call
is stored as normal call in klib, and inlining happens after dependency resolution.
Things become even worse, when instead of changing function was removed or incompatibly changed.
This behavior difference is a problem for library developers, as they need to think for both
compatibility models.
We prefer JVM behavior, because:
- It exists longer and harder to change.
- Klib model is not reasonably implementable on JVM, while JVM model is implementable for klibs.
- JVM model is more intuitive, as people think about inline function as about high-level macros.
- JVM model is simpler ([example](examples/inline-to-crossinline.md))
## Goals and non-goals
This document is supposed to describe
- Current inlining behavior on both JVM and non-JVM backends
- Planned inlining behavior on non-JVM backends
- High-level plan of migration to new behavior for non-JVM backends
- Tricky cases found, while discussing plan
This document is not supposed to describe
- Plans on migrating JVM backend to inlining over IR
- Possible optimizations of inlining process itself in single-module case
## Inlining semantics
The General semantics of inline functions is described [on kotlinlang website](https://kotlinlang.org/docs/inline-functions.html)
and in [this](https://kotlinlang.org/spec/declarations.html#inlining) part of the specification.
In simple cases, behaviour of inline function should be not distinglishable from non-inline, unless
a user is using one of inline function features.
### Inlining lambdas
The most common use-case of inline functions is avoiding creating function objects for callbacks.
```kotlin
inline fun require(condition: Boolean, message: () -> String) {
if (!condition) {
throw IllegalStateException(message())
}
}
fun notNull(data: Any?, name: String) {
// while lambda needs to capture name no object would be allocated
// and nothing would be computed unless data is null
require(data != null) { "Condition failed: $name is null" }
}
```
Also, inlining lambda enables non-local return feature, which is not possible for non-inline functions.
```kotlin
fun test() {
repeat(10) { // inline function from stdlib
if (needReturn()) {
return // this returns from test
}
}
}
```
There is a rarer case of lambda inlining when they are inlined into local class inside inline function.
This case is enabled by crossinline keyword, which makes non-local returns impossible.
```kotlin
inline fun foo(crossinline block: () -> Unit) : Runnable {
return object : Runnable {
override fun run() = block() // block is inlined inside method of local class, which can be called later
}
}
```
Also, lambda inlining behaviour can be opted-out by `noinline` keyword, if required.
### Reified type parameters
Another important feature of inline functions is reified type parameters.
Normally, type parameters are not observable in runtime. You can't get their
class, you can't cast to them, only to upper bounds. That's not true for reified
```kotlin
inline fun <reified U> checkType(x: Any) {
// Normally, this would be unchecked cast, but it's checked here
if (x is U) {
// Normally, you can't get class of type parameter, but you can for reified
println("$x is ${U::class}")
} else {
println("$x is not ${U::class}")
}
}
fun main() {
checkType<String>(2) // would println "2 is not class java.lang.String"
checkType<String>("a") // would print "a is class java.lang.String"
checkType<List<Int>>(listOf("a", "b", "c")) // would print "[a, b, c] is interface java.util.List", oops
}
```
Also, reified type parameters enable creating type-parametrised array creation on JVM.
For example, following function is unimplementable without reified types, as jvm requires
knowing element type in compile time for array creation.
```kotlin
inline fun <reified T> createArray(size: Int, element: T) = Array<T>(size) { element }
```
#### Type erasure
On the other hand, non-reified type parameters continue behaving as their upper bounds.
For example, this code prints `I'm fine`, while would fail with reified type parameter.
```kotlin
inline fun <U> uncheckedCast(x: Any) {
x as U
}
fun main() {
val x = 5
uncheckedCast<String>(x)
println("I'm fine") // would print "I'm fine"
}
```
### Non-JVM backends compilation pipeline overview
```mermaid
---
title: Non-jvm backend pipeline
---
flowchart LR
subgraph First Compiler run
S1[(Common SourceSet)] --> |Source Code| F1[Frontend]
D[(Dependency klib)] --> |Metadata| MDS[Metadata deserializer ]
MDS --> |Deserialized FIR| F1
MDS --> |Deserialized FIR| F2
S2(Platform SourceSet) --> |Source Code| F2[Frontend]
F1 --> |Fir| F2I1[Fir2Ir]
F2 --> |Fir| F2I2[Fir2Ir]
F2I1 --> |IR + LazyIr| IRA[Ir actualizer]
F2I2 --> |IR + LazyIr| IRA
IRA --> |IR + LazyIr| P[Plugins]
P --> |IR + LazyIr| S[Klib Serializer]
S --> |Serialized IR+Metadata| K[(Klib)]
end
D2[(Dependency Klib)] ---> |Serialized IR| DS2[Deserializer]
subgraph Second compiler run
K --> |Serialized IR| DS1[Deserializer]
subgraph irLinker
DS1 --> |IR| L[Linker]
DS2 --> |IR| L[Linker]
end
L --> |IR| PreL[Pre-inline lowerings]
PreL --> |IR| I[Ir Inliner]
I --> |IR| PostL[Lowerings]
PostL --> |IR | CG[Code Gen]
CG --> B[(Final binary)]
end
```
Here is a high-level pipeline of how compilation works for klib-based backends, separated by parts intersting for the purposes of this document.
Let's look at them closer.
The first thing to notice here is that we have two compiler runs.
1. Convert source code to Klib
2. Convert a bunch of Klibs with all dependencies to the final executable binary
From the point of view of the second run, "source code" klib doesn't differ much from dependencies.
Let's define in-memory artifacts we have:
1. **FIR** is frontend representation of kotlin code. Each fir declaration can be serialized to **metadata** and deserialized from it.
2. **LazyIr** is FIR-based IR replacement, which is required to avoid deserialization non-needed parts of dependencies. It contains only declarations, no bodies.
3. **IR** is the most important part here. It is representation parts we are planning to change work with.
Also, let's look closer to the components we have
1. Compilation starts from source code and dependencies. There can be several modules in one compilation,
corresponding to different source-sets (e.g., common and platform). Dependencies are represented as klibs.
2. Dependency Klibs contains serialized frontend representation (a.k.a. metadata), which can be deserialized. Together with source code, it forms input for Frontend
3. For the purposes of this document, Frontend can be seen as a blackbox, converting source code to FIR. It runs on each of source sets independently
4. Fir2Ir converts FIR from source modules to IR, and referenced external Fir to LazyIR.
5. IrActualizer merges all modules to single one, and removes expect classes.
6. Plugins can make some custom changes in IR. This is a stage we don't control well.
7. Klib Serializer converts the resulting IR and FIR to Klib file. Up to some technical details, we can think that Klib is serialized IR + serialized metadata.
8. irLinker is a component that converts a bunch of klibs to IR. It consists of two logical parts, which are not separated in code well.
* Deserializer is a part that can convert klib bytes to IR in memory
* Linker is a part that can match declaration references with declarations.
9. Lowerings are some IR transformations, responsible for making IR simpler, so code generation is able to transform it to final binary. We separate them to three parts, which are not much different now.
* Lowerings happening before inlining
* Inlining lowering itself.
* All further lowerings
10. Code generation is a part that converts lowered IR to final binary. It can be considered as blackbox here.
### JVM backend compilation pipeline overview
Jvm backend doesn't affect our immediate plans, but need to be kept in mind for further work.
```mermaid
---
title: Jvm backend pipeline
---
flowchart LR
S1[(Common SourceSet)] --> |Source Code| F1[Frontend]
D[(Dependency jar)] --> |Metadata + Class Files| MDS[Metadata deserializer ]
MDS --> |Deserialized FIR| F1
MDS --> |Deserialized FIR| F2
S2(Platform SourceSet) --> |Source Code| F2[Frontend]
F1 --> |Fir| F2I1[Fir2Ir]
F2 --> |Fir| F2I2[Fir2Ir]
F2I1 --> |IR + LazyIr| IRA[Ir actualizer]
F2I2 --> |IR + LazyIr| IRA
IRA --> |IR + LazyIr| P[Plugins]
P --> |IR + LazyIR| L[Lowerings]
L --> |IR + LazyIR| CodeGenerator
subgraph CodeGenerator
direction LR
CG[Class File generation]
MG[Metadta generation]
BCI[Bytecode Inliner]
CP[Cororutines processing]
end
CodeGenerator --> |Class Files| B[(Final jar)]
```
This pipeline is exactly the same before Plugins phase, but then, instead of serializing and deserializing IR to klibs,
we are directly passing to lowerings and code generation.
As opposed to non-jvm backends, inlining here happens after all other lowerings as a part of code generation.
Moreover, it doesn't happen over IR, it happens over bytecode.
There is an initiative of moving to IR-based inlining. In that case, it should happen somewhere inside lowerings phase.
Exact implementation of that is out of scope, while it was considered as a part of some decisions.
### IR references kinds
Let's talk about links inside IR. We plan to change significantly how we work with such links, so let's quickly discuss the current state.
We now have two ways of referring IR declaration by another one.
1. Bound `IrSymbol`, which is a direct in-memory link to IrDeclaration object.
2. `IdSignature`, which can be either serialized in klib, or represented as unbound `IrSymbol`. Technically,
sometimes the key for further binding can be not `IdSignauture`, but FIR or Descriptor.
We'd refer IR using type-1 links as **bound**, and IR using type-2 links as **unbound**.
Linker can be thought as a component converting IR with unbound references to Ir with bound references.
This is a complex part, especially with the handling of missing or incompatibly changed declarations.
It can't be done during the first compiler run (klib compilation), because of unclear semantics on this stage (some transitive
dependencies can't be accessed and even may be unknown) and performance reasons.
IdSignature can be though only as unique ID, but not something containing enough information,
which is a problem. It would be much more convenient if it was structured, and some operations can be done with it
(like getting information about call without looking at callee declaration), and linking would happen much closer to code generation.
In that case, this ''new signature'' would be much similar to jvm function descriptor,
and the model would be closer to jvm. Changing this is out-of-scope for the current document.
Most of the compiler pipeline works with bound IR. There are two following exceptions:
1. During IR building IR can be partially unbound, as what-should-be-referred is not created yet.
2. During IR deserialization and linking can be not linked yet.
Working with unbound IR is harder, as we need to avoid using `IrSymbol.owner`.
## Target state
We are aiming to change the pipeline in the near future.
We will leave the JVM pipeline as is, for now.
### New KLib pipeline
```mermaid
---
title: New non-jvm backend pipeline
---
flowchart LR
subgraph First Compiler run
S1[(Common SourceSet)] --> |Source Code| F1[Frontend]
D[(Dependency klib)] --> |Metadata| MDS[Metadata deserializer ]
MDS --> |Deserialized FIR| F1
MDS --> |Deserialized FIR| F2
S2(Platform SourceSet) --> |Source Code| F2[Frontend]
F1 --> |Fir| F2I1[Fir2Ir]
F2 --> |Fir| F2I2[Fir2Ir]
F2I1 --> |IR + LazyIr| IRA[Ir actualizer]
F2I2 --> |IR + LazyIr| IRA
IRA --> |IR + LazyIr| P[Plugins]
P --> |IR + LazyIr| PreL[Pre-inline lowerings]
D --> |Serialized IR| DS3[Deserializer]
DS3 --> |Unbound IR| I
PreL --> |IR| I[Ir Inliner]
I --> |Partially Unbound IR| S[Klib Serializer]
S --> |Serialized IR+Metadata| K[(Klib)]
end
D2[(Dependency Klib)] ---> |Serialized IR| DS2[Deserializer]
subgraph Second compiler run
K --> |Serialized IR| DS1[Deserializer]
subgraph irLinker
DS1 --> |Unbound IR| L[Linker]
DS2 --> |Unbound IR| L[Linker]
end
L --> |IR| PostL[Other Lowerings]
PostL --> |IR | CG[Code Gen]
CG --> B[(Final binary)]
end
```
Let's hightlight important changes from the old version
1. Inlining happens before Klib serialization. That was our main goal
2. Pre-inline lowerings happen before serialization. This puts some [restrictions](#pre-inline-lowerings) on them.
3. Klib serialization must be able to work with partially unlinked IR.
* It should be trivial as signatures are already computed for unbound symbols, but would require some technical work
4. IR inlining must be able to work with unlinked IR in inlined functions.
* It is important that IR on call-site of inline function is always linked.
* Otherwise, would not be able to understand if this call needs to be inlined
* In particular, that means we must inline all inline functions callsites inside inline function before inlining itself.
* Because, callsites inside inline function can be not linked and this contradicts previous assumption
* Our investigation shows that it should be possible, but a lot of new problems can occur here.
### Pre-inline lowerings
There is some work that needs to be done before inlining. Most of this work is lowerings, that get rid of some IR
that would be meaningless or invalid after direct inlining.
Currently, for Native this is:
* Processing [typeOf](examples/typeOf.md) intrinsic
* Processing Array constructor `Array(size:Int, init: (Int) -> Int)`, as it is only inline constructor allowed in language
* Processing of lateinit fields
* We need to do it before inlining, because `isInitialized` intrinsic can access private field, so it should be done
while we in class scope
* Processing of [outer this](examples/outer-this.md)
* Shared variable lowerings
* It is prerequisite for local declarations lowering
* Processing [local declarations](examples/anonimous-objects.md)
* Special handling of function references to inline function with reified type arguments.
There are two important new restrictions.
First, is more technical &mdash; data stored by this lowering in
context would be not available on the second run. At least LateInit and OuterThis lowerings stores something.
We need to get rid of this or move this lowering after inlining
The second is semantic. What happens in this lowering would now be serialized to klibs. This means:
* It can't be ever changed
* If we change the lowering or add a new one, we must be ready to process both versions
* They can't change publicly accessible signatures, because otherwise we must run them on LazyIr when using module as dependency
Because of this, pre-inline lowerings should be avoided when possible.
One more thing, which doesn't exit as separate lowering for now, but probably should be done, is type erasure.
We need too much context to do it, so we can't do it after inline function deserialization. Now it happens during inlining itself.
Here you can check step-by-step examples of how the inlining pipeline should work:
* [Basic](examples/step-by-step-basic.md)
### Debug information
Debug information within Ir is represented by (startOffset/endOffset) pair in IrElement.
Unfortunately, with cross-file inlining it becomes more complex, as it is unclear with respect to which file this offset happens.
This information should be stored in IrInlinedExpression node, and serialized/deserialized.
We need to ensure that all information inside this node can be serialized. It is not true now.
### Migration
**Note**: This is draft decision, more investigation is required.
There are existing klibs with inline functions not prepared for a new scheme.
We plan to leave them as is, i.e., inline them after klib linking.
This is a technical debt we need to accept. It shouldn't be a big issue as
what is required for inlining before linking has much stricter restrictions,
so code working with it should be also able to work with linked IR.
### Corner-cases
There is a bunch of corner cases in inlining. They are not very important for
further document understanding, but must not be lost during implementation.
This creates some restrictions on decisions made, but not required to understand decisions themselves.
You can skip this part on first reading.
- [typeOf](examples/typeOf.md)
- [Interaction with anonymous objects](examples/anonimous-objects.md)
- [Accessing private declarations](examples/accessing-private.md)
- [Calling from java](examples/calling-from-java.md)
- [inline override](examples/inline-override.md)
- [Using declarations not available on call-site](examples/implementation-dependency-chain.md)
## Postponed problems
- Inconsistencies between bytecode and IR inlining (JVM)
- Post-inline optimizations