From 641ba75218ba8504cee9ed814d931b320e9aacde Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 18 Nov 2025 10:52:03 +0100 Subject: [PATCH] Add the EXACTLY_ONCE contract to `suspendCancellableContinuation` `suspendCancellableContinuation` invokes its block exactly once. `suspendCoroutineUninterceptedOrReturn`, which is used for implementing `suspendCancellableContinuation`, already has a contract stating just that, so for consistency and completeness, we add a contract to `suspendCancellableContinuation` itself as well. This will let the compiler recognize that `val`-variables can be safely assigned inside the lambda, as they won't be reassigned or left uninitialized (which would be the case if the lambda executed more than once or possibly not execute at all). There were no use cases actually reported for this. --- .../common/src/CancellableContinuation.kt | 8 ++++++-- .../common/test/CancellableContinuationTest.kt | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 0a6bcb6509..e2cc2d9164 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -1,6 +1,7 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.* +import kotlin.contracts.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -424,10 +425,12 @@ internal fun CancellableContinuation.invokeOnCancellation(handler: Cancel * [CoroutineDispatcher] class, then there is no prompt cancellation guarantee. A custom continuation interceptor * can resume execution of a previously suspended coroutine even if its job was already cancelled. */ +@OptIn(ExperimentalContracts::class) public suspend inline fun suspendCancellableCoroutine( crossinline block: (CancellableContinuation) -> Unit -): T = - suspendCoroutineUninterceptedOrReturn { uCont -> +): T { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + return suspendCoroutineUninterceptedOrReturn { uCont -> val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE) /* * For non-atomic cancellation we setup parent-child relationship immediately @@ -438,6 +441,7 @@ public suspend inline fun suspendCancellableCoroutine( block(cancellable) cancellable.getResult() } +} /** * Suspends the coroutine similar to [suspendCancellableCoroutine], but an instance of diff --git a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt index a47a889431..83b036421f 100644 --- a/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt +++ b/kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt @@ -134,4 +134,15 @@ class CancellableContinuationTest : TestBase() { }) finish(5) } + + /** Tests that the compiler recognizes that [suspendCancellableCoroutine] invokes its block exactly once. */ + @Test + fun testSuspendCancellableCoroutineContract() = runTest { + val i: Int + suspendCancellableCoroutine { cont -> + i = 1 + cont.resume(Unit) + } + assertEquals(1, i) + } } \ No newline at end of file