Skip to content

Commit 397f10e

Browse files
authored
Introduce EXACTLY_ONCE contracts to coroutineScope, supervisorScope, withContext, runBlocking, withTimeout and select (#2030)
1 parent e470df9 commit 397f10e

File tree

9 files changed

+138
-32
lines changed

9 files changed

+138
-32
lines changed

integration/kotlinx-coroutines-jdk8/src/time/Time.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
/*
22
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
4+
@file:OptIn(ExperimentalContracts::class)
5+
46
package kotlinx.coroutines.time
57

68
import kotlinx.coroutines.*
79
import kotlinx.coroutines.flow.*
810
import kotlinx.coroutines.selects.*
911
import java.time.*
1012
import java.time.temporal.*
13+
import kotlin.contracts.*
1114

1215
/**
1316
* "java.time" adapter method for [kotlinx.coroutines.delay].
@@ -35,8 +38,12 @@ public fun <R> SelectBuilder<R>.onTimeout(duration: Duration, block: suspend ()
3538
/**
3639
* "java.time" adapter method for [kotlinx.coroutines.withTimeout].
3740
*/
38-
public suspend fun <T> withTimeout(duration: Duration, block: suspend CoroutineScope.() -> T): T =
39-
kotlinx.coroutines.withTimeout(duration.coerceToMillis(), block)
41+
public suspend fun <T> withTimeout(duration: Duration, block: suspend CoroutineScope.() -> T): T {
42+
contract {
43+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
44+
}
45+
return kotlinx.coroutines.withTimeout(duration.coerceToMillis(), block)
46+
}
4047

4148
/**
4249
* "java.time" adapter method for [kotlinx.coroutines.withTimeoutOrNull].

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

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44

55
@file:JvmMultifileClass
66
@file:JvmName("BuildersKt")
7+
@file:OptIn(ExperimentalContracts::class)
78

89
package kotlinx.coroutines
910

1011
import kotlinx.atomicfu.*
1112
import kotlinx.coroutines.internal.*
1213
import kotlinx.coroutines.intrinsics.*
1314
import kotlinx.coroutines.selects.*
15+
import kotlin.contracts.*
1416
import kotlin.coroutines.*
1517
import kotlin.coroutines.intrinsics.*
1618
import kotlin.jvm.*
@@ -134,31 +136,36 @@ private class LazyDeferredCoroutine<T>(
134136
public suspend fun <T> withContext(
135137
context: CoroutineContext,
136138
block: suspend CoroutineScope.() -> T
137-
): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
138-
// compute new context
139-
val oldContext = uCont.context
140-
val newContext = oldContext + context
141-
// always check for cancellation of new context
142-
newContext.checkCompletion()
143-
// FAST PATH #1 -- new context is the same as the old one
144-
if (newContext === oldContext) {
145-
val coroutine = ScopeCoroutine(newContext, uCont)
146-
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
139+
): T {
140+
contract {
141+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
147142
}
148-
// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
149-
// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
150-
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
151-
val coroutine = UndispatchedCoroutine(newContext, uCont)
152-
// There are changes in the context, so this thread needs to be updated
153-
withCoroutineContext(newContext, null) {
143+
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
144+
// compute new context
145+
val oldContext = uCont.context
146+
val newContext = oldContext + context
147+
// always check for cancellation of new context
148+
newContext.checkCompletion()
149+
// FAST PATH #1 -- new context is the same as the old one
150+
if (newContext === oldContext) {
151+
val coroutine = ScopeCoroutine(newContext, uCont)
154152
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
155153
}
154+
// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
155+
// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
156+
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
157+
val coroutine = UndispatchedCoroutine(newContext, uCont)
158+
// There are changes in the context, so this thread needs to be updated
159+
withCoroutineContext(newContext, null) {
160+
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
161+
}
162+
}
163+
// SLOW PATH -- use new dispatcher
164+
val coroutine = DispatchedCoroutine(newContext, uCont)
165+
coroutine.initParentJob()
166+
block.startCoroutineCancellable(coroutine, coroutine)
167+
coroutine.getResult()
156168
}
157-
// SLOW PATH -- use new dispatcher
158-
val coroutine = DispatchedCoroutine(newContext, uCont)
159-
coroutine.initParentJob()
160-
block.startCoroutineCancellable(coroutine, coroutine)
161-
coroutine.getResult()
162169
}
163170

164171
/**

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/*
22
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
4+
@file:OptIn(ExperimentalContracts::class)
45

56
package kotlinx.coroutines
67

78
import kotlinx.coroutines.internal.*
89
import kotlinx.coroutines.intrinsics.*
10+
import kotlin.contracts.*
911
import kotlin.coroutines.*
1012
import kotlin.coroutines.intrinsics.*
1113

@@ -183,11 +185,15 @@ public object GlobalScope : CoroutineScope {
183185
* or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
184186
* (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
185187
*/
186-
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R =
187-
suspendCoroutineUninterceptedOrReturn { uCont ->
188+
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
189+
contract {
190+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
191+
}
192+
return suspendCoroutineUninterceptedOrReturn { uCont ->
188193
val coroutine = ScopeCoroutine(uCont.context, uCont)
189194
coroutine.startUndispatchedOrReturn(coroutine, block)
190195
}
196+
}
191197

192198
/**
193199
* Creates a [CoroutineScope] that wraps the given coroutine [context].

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/*
22
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
4-
4+
@file:OptIn(ExperimentalContracts::class)
55
@file:Suppress("DEPRECATION_ERROR")
66

77
package kotlinx.coroutines
88

99
import kotlinx.coroutines.internal.*
1010
import kotlinx.coroutines.intrinsics.*
11+
import kotlin.contracts.*
1112
import kotlin.coroutines.*
1213
import kotlin.coroutines.intrinsics.*
1314
import kotlin.jvm.*
@@ -47,11 +48,15 @@ public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent)
4748
* A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children,
4849
* but does not cancel parent job.
4950
*/
50-
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R =
51-
suspendCoroutineUninterceptedOrReturn { uCont ->
51+
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
52+
contract {
53+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
54+
}
55+
return suspendCoroutineUninterceptedOrReturn { uCont ->
5256
val coroutine = SupervisorCoroutine(uCont.context, uCont)
5357
coroutine.startUndispatchedOrReturn(coroutine, block)
5458
}
59+
}
5560

5661
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
5762
override fun childCancelled(cause: Throwable): Boolean = false

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/*
22
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
4+
@file:OptIn(ExperimentalContracts::class)
45

56
package kotlinx.coroutines
67

78
import kotlinx.coroutines.internal.*
89
import kotlinx.coroutines.intrinsics.*
910
import kotlinx.coroutines.selects.*
11+
import kotlin.contracts.*
1012
import kotlin.coroutines.*
1113
import kotlin.coroutines.intrinsics.*
1214
import kotlin.jvm.*
@@ -27,6 +29,9 @@ import kotlin.time.*
2729
* @param timeMillis timeout time in milliseconds.
2830
*/
2931
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
32+
contract {
33+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
34+
}
3035
if (timeMillis <= 0L) throw TimeoutCancellationException("Timed out immediately")
3136
return suspendCoroutineUninterceptedOrReturn { uCont ->
3237
setupTimeout(TimeoutCoroutine(timeMillis, uCont), block)
@@ -46,8 +51,12 @@ public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineSco
4651
* Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher].
4752
*/
4853
@ExperimentalTime
49-
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T =
50-
withTimeout(timeout.toDelayMillis(), block)
54+
public suspend fun <T> withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T {
55+
contract {
56+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
57+
}
58+
return withTimeout(timeout.toDelayMillis(), block)
59+
}
5160

5261
/**
5362
* Runs a given suspending block of code inside a coroutine with a specified [timeout][timeMillis] and returns

kotlinx-coroutines-core/common/src/selects/Select.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
4+
@file:OptIn(ExperimentalContracts::class)
45

56
package kotlinx.coroutines.selects
67

@@ -10,6 +11,7 @@ import kotlinx.coroutines.channels.*
1011
import kotlinx.coroutines.internal.*
1112
import kotlinx.coroutines.intrinsics.*
1213
import kotlinx.coroutines.sync.*
14+
import kotlin.contracts.*
1315
import kotlin.coroutines.*
1416
import kotlin.coroutines.intrinsics.*
1517
import kotlin.jvm.*
@@ -199,8 +201,11 @@ public interface SelectInstance<in R> {
199201
* Note that this function does not check for cancellation when it is not suspended.
200202
* Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed.
201203
*/
202-
public suspend inline fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R =
203-
suspendCoroutineUninterceptedOrReturn { uCont ->
204+
public suspend inline fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R {
205+
contract {
206+
callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
207+
}
208+
return suspendCoroutineUninterceptedOrReturn { uCont ->
204209
val scope = SelectBuilderImpl(uCont)
205210
try {
206211
builder(scope)
@@ -209,6 +214,7 @@ public suspend inline fun <R> select(crossinline builder: SelectBuilder<R>.() ->
209214
}
210215
scope.getResult()
211216
}
217+
}
212218

213219

214220
@SharedImmutable
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import kotlinx.coroutines.selects.*
8+
import kotlin.test.*
9+
10+
class BuilderContractsTest : TestBase() {
11+
12+
@Test
13+
fun testContracts() = runTest {
14+
// Coroutine scope
15+
val cs: Int
16+
coroutineScope {
17+
cs = 42
18+
}
19+
consume(cs)
20+
21+
// Supervisor scope
22+
val svs: Int
23+
supervisorScope {
24+
svs = 21
25+
}
26+
consume(svs)
27+
28+
// with context scope
29+
val wctx: Int
30+
withContext(Dispatchers.Unconfined) {
31+
wctx = 239
32+
}
33+
consume(wctx)
34+
35+
val wt: Int
36+
withTimeout(Long.MAX_VALUE) {
37+
wt = 123
38+
}
39+
consume(wt)
40+
41+
val s: Int
42+
select<Unit> {
43+
s = 42
44+
Job().apply { complete() }.onJoin {}
45+
}
46+
consume(s)
47+
}
48+
49+
private fun consume(a: Int) {
50+
a.hashCode() // BE codegen verification
51+
}
52+
}

kotlinx-coroutines-core/jvm/src/Builders.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
@file:JvmMultifileClass
66
@file:JvmName("BuildersKt")
7+
@file:OptIn(ExperimentalContracts::class)
78

89
package kotlinx.coroutines
910

1011
import java.util.concurrent.locks.*
12+
import kotlin.contracts.*
1113
import kotlin.coroutines.*
1214

1315
/**
@@ -34,6 +36,9 @@ import kotlin.coroutines.*
3436
*/
3537
@Throws(InterruptedException::class)
3638
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
39+
contract {
40+
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
41+
}
3742
val currentThread = Thread.currentThread()
3843
val contextInterceptor = context[ContinuationInterceptor]
3944
val eventLoop: EventLoop?

kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,13 @@ class RunBlockingTest : TestBase() {
162162

163163
handle.dispose()
164164
}
165+
166+
@Test
167+
fun testContract() {
168+
val rb: Int
169+
runBlocking {
170+
rb = 42
171+
}
172+
rb.hashCode() // unused
173+
}
165174
}

0 commit comments

Comments
 (0)