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>
}
```