Oh great, I think I managed to trigger KT-79276 ag...
# coroutines
r
Oh great, I think I managed to trigger KT-79276 again on 2.2.21 🙃
bug.kt
It's somewhere in this huge piece of code, will see if I can reduce it
i
Hi! Is it possible to attach the full project?
r
No, proprietary 😞 I'll try to reduce it.
thank you color 1
Looks like the LLM was correct, removing this code makes it compile:
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            if (autoCardScanEnabled != enabled) {
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
                if (!response.isSuccessful() && enabled) {
                    autoCardScanEnabled = false
                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
                } else {
                    // somehow disabling it always returns a fail
                    autoCardScanEnabled = enabled
                }
                delay(100.milliseconds)
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
        }
    }
i
Thank you! Could you attach a stripped code for
autoCardScanLock
?
r
Working on it
It's basically
Copy code
private val autoCardScanLock = Mutex()
thank you color 1
Great, that seems to compile...
Condensated sample, can't reproduce it here:
Copy code
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedSendChannelException
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.channels.ticker
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import me.getreach.compilerbug.ui.theme.CompilerBugTheme
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.time.Duration.Companion.milliseconds

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            CompilerBugTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    CompilerBugTheme {
        Greeting("Android")
    }
}
@OptIn(DelicateCoroutinesApi::class, ObsoleteCoroutinesApi::class)
data class MessageProcessor(val job: Job) {
    val TAG = "MessageProcessor"

    private val requests = Channel<Command>(100, BufferOverflow.DROP_OLDEST, onUndeliveredElement = { it.callback.cancel(
        CancellationException("Too many requests have been stacked.")
    )})

    fun sendCommand(command: Command) {
        requests.trySend(command).onFailure {
            print(it)
        }
    }

    private var autoCardScanEnabled = false
    private val autoCardScanLock = Mutex()

    suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            if (autoCardScanEnabled != enabled) {
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.PingCard(continuation, 0b00))
                }
                if (!response.isSuccessful() && enabled) {
                    autoCardScanEnabled = false
                    throw Exception("Auto card scan request failed: ${response.error}")
                } else {
                    // somehow disabling it always returns a fail
                    autoCardScanEnabled = enabled
                }
                delay(100.milliseconds)
            } else {
                println("Card Scan already in requested state")
            }
        }
    }

    suspend fun sendCloseRf() {
        requestAutoCardScan(0, 0, false)
    }

    suspend fun onMessage(message: ParsingState.ParsingSuccessful) {
    }
}

sealed class Command {
    val channel: Channel<ParsingState.ParsingSuccessful> = Channel(1)
    abstract val callback: CancellableContinuation<ParsingState.ParsingSuccessful>
    abstract val commandByte: Byte
    abstract fun toBytes(comByteManager: ComByteManager): ByteArray

    data class PingCard(
        override val callback: CancellableContinuation<ParsingState.ParsingSuccessful>,
        val cardType: Byte,
    ): Command() {
        override val commandByte: Byte = 0b00
        override fun toBytes(comByteManager: ComByteManager): ByteArray = byteArrayOf()
    }
}

sealed interface ParsingState {
    data class ParsingInProgress(val cmd: Byte, val data: ByteArray, val error: ByteArray, val numberOfFrames: Byte, val frameNumber: Byte): ParsingState
    data class ParsingSuccessful(val cmd: Byte, val data: ByteArray, val error: ByteArray): ParsingState {
        fun isSuccessful(): Boolean {
            return error[0] == 0x90.toByte()
        }
    }
    data object NoData: ParsingState
    sealed class ParsingFailure(override val message: String?): Exception(message), ParsingState
    data class WrongSerialNumber(val expected: Int, val got: Byte): ParsingFailure("Wrong serial number. Got: $got, expected: $expected")
    data class MessageTooShort(val expected: Int, val got: Int): ParsingFailure("Message too short. Got: $got, expected: $expected")
    data class InvalidFrameTag(val got: Byte): ParsingFailure("Invalid frame tag. Got: $got")
    data class NoPriorStartMessage(val got: Byte): ParsingFailure("Not in ParsingInProgress, but got a follow-up frame: $got")
}
data class ComByteManager(var isBle5: Boolean = false) { }
🤔 1
No, it's not the
withLock
call 🤔
Works:
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
//        autoCardScanLock.withLock {
            if (autoCardScanEnabled != enabled) {
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
//                if (!response.isSuccessful() && enabled) {
//                    autoCardScanEnabled = false
//                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
//                } else {
//                    // somehow disabling it always returns a fail
//                    autoCardScanEnabled = enabled
//                }
//                delay(100.milliseconds)
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
//        }
    }
Compiles:
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            if (autoCardScanEnabled != enabled) {
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
//                if (!response.isSuccessful() && enabled) {
//                    autoCardScanEnabled = false
//                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
//                } else {
//                    // somehow disabling it always returns a fail
//                    autoCardScanEnabled = enabled
//                }
//                delay(100.milliseconds)
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
        }
    }
Compiles:
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            if (autoCardScanEnabled != enabled) {
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
                print(response)
//                if (!response.isSuccessful() && enabled) {
//                    autoCardScanEnabled = false
//                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
//                } else {
//                    // somehow disabling it always returns a fail
//                    autoCardScanEnabled = enabled
//                }
//                delay(100.milliseconds)
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
        }
    }
I suspect having Hilt/Dagger in there could complicate things too
Sadly one of the dependencies requires it 😭
Compiles:
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            if (autoCardScanEnabled != enabled) {
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
                if (!response.isSuccessful() && enabled) {
                    autoCardScanEnabled = false
                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
                } else {
                    // somehow disabling it always returns a fail
                    autoCardScanEnabled = enabled
                }
//                delay(100.milliseconds)
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
        }
    }
Adding the delay back in triggers the bug 🤨
If I move the delay as follows, it compiles:
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            if (autoCardScanEnabled != enabled) {
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
                if (!response.isSuccessful() && enabled) {
                    autoCardScanEnabled = false
                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
                } else {
                    // somehow disabling it always returns a fail
                    autoCardScanEnabled = enabled
                }
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
            delay(100.milliseconds)
        }
    }
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            var shouldWait = false
            if (autoCardScanEnabled != enabled) {
                shouldWait = true
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
                if (!response.isSuccessful() && enabled) {
                    autoCardScanEnabled = false
                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
                } else {
                    // somehow disabling it always returns a fail
                    autoCardScanEnabled = enabled
                }
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
            if (shouldWait) {
                delay(100.milliseconds)
            }
        }
    }
^ this compiles 🤷
thank you color 1
This fails:
Copy code
suspend fun requestAutoCardScan(delayMs: Byte, cardType: Byte, enabled: Boolean = true) {
        Log.d(TAG, "Automatic search for cards requested: $enabled, state: $autoCardScanEnabled")
        autoCardScanLock.withLock {
            // this is a workaround for a compiler bug in kotlin 2.2.21
            var shouldWait = false
            if (autoCardScanEnabled != enabled) {
                shouldWait = true
                val response = suspendCancellableCoroutine { continuation ->
                    sendCommand(Command.AutoCardSearch(continuation, enabled, delayMs, cardType))
                }
                if (!response.isSuccessful() && enabled) {
                    autoCardScanEnabled = false
                    throw Exception("Auto card scan request failed: ${response.error.dump()}")
                } else {
                    // somehow disabling it always returns a fail
                    autoCardScanEnabled = enabled
                }
                delay(100.milliseconds)
            } else {
                Timber.d { "Card Scan already in requested state" }
            }
            if (shouldWait) {
            }
        }
    }