Skip to content

Commit d1c583a

Browse files
authored
Add the EXACTLY_ONCE contract to suspendCancellableContinuation (#4574)
`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.
1 parent ebad555 commit d1c583a

File tree

2 files changed

+17
-2
lines changed

2 files changed

+17
-2
lines changed

kotlinx-coroutines-core/common/src/CancellableContinuation.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kotlinx.coroutines
22

33
import kotlinx.coroutines.internal.*
4+
import kotlin.contracts.*
45
import kotlin.coroutines.*
56
import kotlin.coroutines.intrinsics.*
67

@@ -424,10 +425,12 @@ internal fun <T> CancellableContinuation<T>.invokeOnCancellation(handler: Cancel
424425
* [CoroutineDispatcher] class, then there is no prompt cancellation guarantee. A custom continuation interceptor
425426
* can resume execution of a previously suspended coroutine even if its job was already cancelled.
426427
*/
428+
@OptIn(ExperimentalContracts::class)
427429
public suspend inline fun <T> suspendCancellableCoroutine(
428430
crossinline block: (CancellableContinuation<T>) -> Unit
429-
): T =
430-
suspendCoroutineUninterceptedOrReturn { uCont ->
431+
): T {
432+
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
433+
return suspendCoroutineUninterceptedOrReturn { uCont ->
431434
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
432435
/*
433436
* For non-atomic cancellation we setup parent-child relationship immediately
@@ -438,6 +441,7 @@ public suspend inline fun <T> suspendCancellableCoroutine(
438441
block(cancellable)
439442
cancellable.getResult()
440443
}
444+
}
441445

442446
/**
443447
* Suspends the coroutine similar to [suspendCancellableCoroutine], but an instance of

kotlinx-coroutines-core/common/test/CancellableContinuationTest.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,15 @@ class CancellableContinuationTest : TestBase() {
134134
})
135135
finish(5)
136136
}
137+
138+
/** Tests that the compiler recognizes that [suspendCancellableCoroutine] invokes its block exactly once. */
139+
@Test
140+
fun testSuspendCancellableCoroutineContract() = runTest {
141+
val i: Int
142+
suspendCancellableCoroutine { cont ->
143+
i = 1
144+
cont.resume(Unit)
145+
}
146+
assertEquals(1, i)
147+
}
137148
}

0 commit comments

Comments
 (0)