Kotlin 2 not calling the last elvis operator in a coroutine

Elvis operations do not work in tests using UnconfinedTestDispatcher when using kotlin 2.0.20. It works with kotlin 1.9.25. Has something changed in kotlin 2 or is this a bug?
It works if I move the contents of the collect block in a separate function.

Below is a simplified example of ?: run not being called.

fun fail() {
    runTest {
        var result: String = "fail"
        val job = this.launch(UnconfinedTestDispatcher(testScheduler)) {                
                println("do some processing with the data")
                "test".let {
                        null
                    } ?: run { // this is not called if it is the last thing in the collect block                        
                    	result = "success"
                        println("This is not called when using kotlin 2.0.20!")                        
                    }                            
        }
        dataFlow.emit("one")
        job.join()
        println("Result is $result")
    }
}

I tested this with kotlin playground:

Edited: Removed the flow collecting as it seemst that it does not need to be in a flow collect.

It seems that it doesn’t even work with normal runBlocking

    runBlocking {
        var result: String = "fail"
        val job = this.launch {
            println("Collect a flow and do some processing with the data")
            "test".let {
                    null
                } ?: run { // this is not called if it is the last thing in the collect block                        
                    result = "success"
                    println("This is not called when using kotlin 2.0.20!")                        
                }                            
        }

        job.join()
        println("Result is $result")
    }

It looks like some kind of a bug in the compiler.

To add to the mystery, specifying types explicitly works for some reason

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        var result: String = "fail"
        val job = this.launch {
            println("Collect a flow and do some processing with the data")
            "test".let<String, Nothing?> {
                    null
                } ?: run {
                    result = "success"
                    println("This is not called when using kotlin 2.0.20!")                        
                }                            
        }

        job.join()
        println("Result is $result")
    }
}

Ohhhh, I think I figured it out! It has to do with implicitly returning Unit out of a lambda.
Consider this code:

import kotlinx.coroutines.*

fun main() {
    runBlocking {
        var result: String = "fail"
        val job = this.launch {
            println("Collect a flow and do some processing with the data")
            "test".let<_, Unit> {
                    null
                } ?: run {
                    result = "success"
                    println("This is not called when using kotlin 2.0.20!")                        
                }
        }

        job.join()
        println("Result is $result")
    }
}

Obviously, it’s expected here that “Result is fail” would get printed, because Unit is not null. This seems like a type inference edge case where, because launch expects a Unit to be returned, let’s block is also expected to return Unit, which is fine because that implicit return exists. That’s why it only occurs for the last expression in a launch block.

Small reproducer:

fun main() = run<Unit> {
    run { null } ?: println("OK!")
}

Hitting “Generate JS” on the Kotlin Playground seems to confirm this hypothesis because the main function contains the following:

function main() {
  // Inline function 'kotlin.run' call
  // Inline function 'kotlin.contracts.contract' call
  // Inline function 'kotlin.run' call
  // Inline function 'kotlin.contracts.contract' call
  if (Unit_instance == null) {
    // Inline function 'kotlin.run' call
    // Inline function 'kotlin.contracts.contract' call
    println('OK!');
  }
  return Unit_instance;
}

Edit: I found another reproducer that makes somewhat less sense type-inference wise:

fun main() = run<Unit> {
    run { null } ?: "OK!".also(::println)
}

The RHS of the elvis is clearly not Unit, so it’s strange that the LHS is inferred to be Unit

There is bug of this now:
https://youtrack.jetbrains.com/issue/KT-71751/K2-regression-Skipping-code-in-the-last-statement-of-a-lambda

1 Like

Nice catch!

Almost always I use nullable?.let { something } ?: fallback() so I wouldn’t notice this, but it is a bug indeed.