Add documentation on planned inline changes.
^KT-64570
This commit is contained in:
committed by
Space Team
parent
09713bb89e
commit
01c16ed736
@@ -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.
|
||||
@@ -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?
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user