Tailrec makes suspend fun never ends

Hi, I’m learning how to make coroutines work with Java libraries using CompletableFuture. Below is my code:

// x invokes y invokes z invokes Java client
suspend fun x(i: Int, client: FakeJavaClient): Int {

    fun z(k: Int): Int {
        println("z: $k")
        return client.query(k).get()
    }

    tailrec // with 'tailrec', the code never terminates
    suspend fun y(j: Int): Int {
        val ret = z(j)
        if (ret > 10) {
            return ret
        }

        return y(j + 1)
    }

    return y(i)
}

fun main()  {
    runBlocking {
        launch(Dispatchers.IO) {
            FakeJavaClient().use { x(0, it) }
        }
    }
    println("Done")
}

class FakeJavaClient : AutoCloseable {
    private val executor = Executors.newFixedThreadPool(10)

    fun query(i: Int): CompletableFuture<Int> {
        val f = CompletableFuture<Int>()
        executor.submit {
            Thread.sleep(1000)
            f.complete(i * 2)
        }
        return f
    }

    override fun close() {
        executor.shutdown()
        executor.awaitTermination(10, TimeUnit.SECONDS)
    }
}

If I add the tailrec modifier to function y, the code outputs like below and never ends:

z: 0
z: 0
z: 0
z: 0
z: 0
...

If I remove tailrec on y, the code behaves as my expectation

z: 0
z: 1
z: 2
z: 3
z: 4
z: 5
z: 6
Done

Could someone kindly help me understand what happens here ?

This looks like a bug to me. Tailrec should only be an optimization. You may be able to find out what’s going on by doing both:

  • Reduce the problem into a minimal version (the current version includes many other complexities)
  • Look at the generated bytecode (easier if you did the above, so there’s less to look at) - the decompiled version may be helpful (or not)
1 Like

You should also create an issue at https://kotl.in/issue