Kotlin K2 Compiler will improve your expression design

Kotlin 2.0 is finally arrived and become stable. The most interesting upgrade to Kotlin 2.0 is the K2 compiler, it helps your project build faster and allow you to write smarter codes.

In the recent JetBrain blog post, it shows Kotlin 2.0.0 (K2) vs 1.9.23 performance gains:

  • Clean build: 94% faster (57.7s → 29.7s)
  • Initialization: 488% faster (0.126s → 0.022s)
  • Analysis: 376% faster (0.581s → 0.122s)

Not only the performance is significant improved, the smart cast handling on K2 compiler also significantly improved. I consolidated five examples:

💡
You can test the following examples on https://play.kotlinlang.org/

Handling generic box interface

interface Box<T>
val <X> Box<X>.first: X get() = TODO()

fun foo(p: Box<() -> Unit>) {
    // K1: compile error
    // K2: ok
    p.first()
    p.first.invoke()
}

Synthetic data flow variables can carry information about smart-casts

fun test(foo: String?) {
    if (foo != null) {
        foo.length
    }
    
    val fooIsNonNull = foo != null
    if (fooIsNonNull) {
        // K1: compile error , unsafe call
        // K2: ok
        foo.length
    }
}

Smart-casts inside changing closures of lambdas

  • In general, we don't know anything about the inPlaceRun function
  • The parameter f could be invoked later or just stored somewhere, and value could be changed after the lambda block
fun inPlaceRun(f: () → Unit) {
    f()
}

fun foo() {
    var value: Int? = null
    inPlaceRun {
        // K1: compile error, smart cast to 'Int' is impossible
        // K2: ok
        if (value != null) value++ else value = 0
    }
}
K1

Explicitly tell the compiler that the parameter f will be invoked only within the method and won't be stored anywhere else.

@OptIn(ExperimentalContracts::class)
fun inPlaceRun(f: () → Unit) {
    contract {
        callsInPlace(f, InvocationKind.UNKNOWN)
    }
    f()
}

fun foo() {
    var value: Int? = null
    inPlaceRun {
        // K2: smart cast to 'Int'
        if (value != null) value++ else value = 0
    }
K2

Smart-casts inside closures of inline lambdas

  • In K1, it's impossible to have a smart-cast because the lambda can be invoked later, even after forEachIndexed call
fun indexOfMax(a: IntArray): Int? {
    var maxI: Int? = null
    a.forEachIndexed { i, value ->
        // K1: compile error, smart cast to 'Int' is impossible,
        // because 'maxI' is a local variable that is captured by a changing closure
        // K2: Ok
        if (maxI == null || a[maxI] <= value) {
            maxI = i
        }
    }
    return maxI
}

Smart-casts after ||: merge to a common supertype

K2 compiler now can recognize || case in the condition. You don't need to write switch case now.

interface Status {
    fun signal()
}

interface Ok : Status
interface Postponed : Status
interface Declined : Status

fun foo(o: Any) {
    if (o is Postponed || o is Declined) {
        // K1: compiler error, o is inferred to Any
        // K2: o is inferred to Status
        o.signal()
    }
}