From eb0f41e118a5f28e6f8dfdccaea2333ca440a250 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 15 May 2020 14:36:59 +0300 Subject: [PATCH] Introduce EXACTLY_ONCE contracts to coroutineScope, supervisorScope, withContext, runBlocking, withTimeout and select --- .../kotlinx-coroutines-jdk8/src/time/Time.kt | 11 +++- .../common/src/Builders.common.kt | 49 +++++++++-------- .../common/src/CoroutineScope.kt | 10 +++- .../common/src/Supervisor.kt | 11 ++-- kotlinx-coroutines-core/common/src/Timeout.kt | 13 ++++- .../common/src/selects/Select.kt | 10 +++- .../common/test/BuilderContractsTest.kt | 52 +++++++++++++++++++ kotlinx-coroutines-core/jvm/src/Builders.kt | 5 ++ .../jvm/test/RunBlockingTest.kt | 9 ++++ 9 files changed, 138 insertions(+), 32 deletions(-) create mode 100644 kotlinx-coroutines-core/common/test/BuilderContractsTest.kt diff --git a/integration/kotlinx-coroutines-jdk8/src/time/Time.kt b/integration/kotlinx-coroutines-jdk8/src/time/Time.kt index 807a3bbc3b..acff1d21cb 100644 --- a/integration/kotlinx-coroutines-jdk8/src/time/Time.kt +++ b/integration/kotlinx-coroutines-jdk8/src/time/Time.kt @@ -1,6 +1,8 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalContracts::class) + package kotlinx.coroutines.time import kotlinx.coroutines.* @@ -8,6 +10,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.selects.* import java.time.* import java.time.temporal.* +import kotlin.contracts.* /** * "java.time" adapter method for [kotlinx.coroutines.delay]. @@ -35,8 +38,12 @@ public fun SelectBuilder.onTimeout(duration: Duration, block: suspend () /** * "java.time" adapter method for [kotlinx.coroutines.withTimeout]. */ -public suspend fun withTimeout(duration: Duration, block: suspend CoroutineScope.() -> T): T = - kotlinx.coroutines.withTimeout(duration.coerceToMillis(), block) +public suspend fun withTimeout(duration: Duration, block: suspend CoroutineScope.() -> T): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return kotlinx.coroutines.withTimeout(duration.coerceToMillis(), block) +} /** * "java.time" adapter method for [kotlinx.coroutines.withTimeoutOrNull]. diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index 7dd1b174ee..64bff500dc 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -4,6 +4,7 @@ @file:JvmMultifileClass @file:JvmName("BuildersKt") +@file:OptIn(ExperimentalContracts::class) package kotlinx.coroutines @@ -11,6 +12,7 @@ import kotlinx.atomicfu.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* +import kotlin.contracts.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* import kotlin.jvm.* @@ -134,31 +136,36 @@ private class LazyDeferredCoroutine( public suspend fun withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T -): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont -> - // compute new context - val oldContext = uCont.context - val newContext = oldContext + context - // always check for cancellation of new context - newContext.checkCompletion() - // FAST PATH #1 -- new context is the same as the old one - if (newContext === oldContext) { - val coroutine = ScopeCoroutine(newContext, uCont) - return@sc coroutine.startUndispatchedOrReturn(coroutine, block) +): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed) - // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher) - if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) { - val coroutine = UndispatchedCoroutine(newContext, uCont) - // There are changes in the context, so this thread needs to be updated - withCoroutineContext(newContext, null) { + return suspendCoroutineUninterceptedOrReturn sc@ { uCont -> + // compute new context + val oldContext = uCont.context + val newContext = oldContext + context + // always check for cancellation of new context + newContext.checkCompletion() + // FAST PATH #1 -- new context is the same as the old one + if (newContext === oldContext) { + val coroutine = ScopeCoroutine(newContext, uCont) return@sc coroutine.startUndispatchedOrReturn(coroutine, block) } + // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed) + // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher) + if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) { + val coroutine = UndispatchedCoroutine(newContext, uCont) + // There are changes in the context, so this thread needs to be updated + withCoroutineContext(newContext, null) { + return@sc coroutine.startUndispatchedOrReturn(coroutine, block) + } + } + // SLOW PATH -- use new dispatcher + val coroutine = DispatchedCoroutine(newContext, uCont) + coroutine.initParentJob() + block.startCoroutineCancellable(coroutine, coroutine) + coroutine.getResult() } - // SLOW PATH -- use new dispatcher - val coroutine = DispatchedCoroutine(newContext, uCont) - coroutine.initParentJob() - block.startCoroutineCancellable(coroutine, coroutine) - coroutine.getResult() } /** diff --git a/kotlinx-coroutines-core/common/src/CoroutineScope.kt b/kotlinx-coroutines-core/common/src/CoroutineScope.kt index 7b5c645d1f..11115a8800 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineScope.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineScope.kt @@ -1,11 +1,13 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalContracts::class) package kotlinx.coroutines import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* +import kotlin.contracts.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -183,11 +185,15 @@ public object GlobalScope : CoroutineScope { * or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope * (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope). */ -public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R = - suspendCoroutineUninterceptedOrReturn { uCont -> +public suspend fun coroutineScope(block: suspend CoroutineScope.() -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return suspendCoroutineUninterceptedOrReturn { uCont -> val coroutine = ScopeCoroutine(uCont.context, uCont) coroutine.startUndispatchedOrReturn(coroutine, block) } +} /** * Creates a [CoroutineScope] that wraps the given coroutine [context]. diff --git a/kotlinx-coroutines-core/common/src/Supervisor.kt b/kotlinx-coroutines-core/common/src/Supervisor.kt index 1991119053..542e4fef48 100644 --- a/kotlinx-coroutines-core/common/src/Supervisor.kt +++ b/kotlinx-coroutines-core/common/src/Supervisor.kt @@ -1,13 +1,14 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - +@file:OptIn(ExperimentalContracts::class) @file:Suppress("DEPRECATION_ERROR") package kotlinx.coroutines import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* +import kotlin.contracts.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* import kotlin.jvm.* @@ -47,11 +48,15 @@ public fun SupervisorJob0(parent: Job? = null) : Job = SupervisorJob(parent) * A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children, * but does not cancel parent job. */ -public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R): R = - suspendCoroutineUninterceptedOrReturn { uCont -> +public suspend fun supervisorScope(block: suspend CoroutineScope.() -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return suspendCoroutineUninterceptedOrReturn { uCont -> val coroutine = SupervisorCoroutine(uCont.context, uCont) coroutine.startUndispatchedOrReturn(coroutine, block) } +} private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) { override fun childCancelled(cause: Throwable): Boolean = false diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 87fe733773..c8e4455c92 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -1,12 +1,14 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalContracts::class) package kotlinx.coroutines import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* +import kotlin.contracts.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* import kotlin.jvm.* @@ -27,6 +29,9 @@ import kotlin.time.* * @param timeMillis timeout time in milliseconds. */ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } if (timeMillis <= 0L) throw TimeoutCancellationException("Timed out immediately") return suspendCoroutineUninterceptedOrReturn { uCont -> setupTimeout(TimeoutCoroutine(timeMillis, uCont), block) @@ -46,8 +51,12 @@ public suspend fun withTimeout(timeMillis: Long, block: suspend CoroutineSco * Implementation note: how the time is tracked exactly is an implementation detail of the context's [CoroutineDispatcher]. */ @ExperimentalTime -public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T = - withTimeout(timeout.toDelayMillis(), block) +public suspend fun withTimeout(timeout: Duration, block: suspend CoroutineScope.() -> T): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return withTimeout(timeout.toDelayMillis(), block) +} /** * Runs a given suspending block of code inside a coroutine with a specified [timeout][timeMillis] and returns diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 0595341f92..e744a0c724 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -1,6 +1,7 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:OptIn(ExperimentalContracts::class) package kotlinx.coroutines.selects @@ -10,6 +11,7 @@ import kotlinx.coroutines.channels.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.sync.* +import kotlin.contracts.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* import kotlin.jvm.* @@ -199,8 +201,11 @@ public interface SelectInstance { * Note that this function does not check for cancellation when it is not suspended. * Use [yield] or [CoroutineScope.isActive] to periodically check for cancellation in tight loops if needed. */ -public suspend inline fun select(crossinline builder: SelectBuilder.() -> Unit): R = - suspendCoroutineUninterceptedOrReturn { uCont -> +public suspend inline fun select(crossinline builder: SelectBuilder.() -> Unit): R { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + return suspendCoroutineUninterceptedOrReturn { uCont -> val scope = SelectBuilderImpl(uCont) try { builder(scope) @@ -209,6 +214,7 @@ public suspend inline fun select(crossinline builder: SelectBuilder.() -> } scope.getResult() } +} @SharedImmutable diff --git a/kotlinx-coroutines-core/common/test/BuilderContractsTest.kt b/kotlinx-coroutines-core/common/test/BuilderContractsTest.kt new file mode 100644 index 0000000000..b20dd6b1d2 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/BuilderContractsTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.selects.* +import kotlin.test.* + +class BuilderContractsTest : TestBase() { + + @Test + fun testContracts() = runTest { + // Coroutine scope + val cs: Int + coroutineScope { + cs = 42 + } + consume(cs) + + // Supervisor scope + val svs: Int + supervisorScope { + svs = 21 + } + consume(svs) + + // with context scope + val wctx: Int + withContext(Dispatchers.Unconfined) { + wctx = 239 + } + consume(wctx) + + val wt: Int + withTimeout(Long.MAX_VALUE) { + wt = 123 + } + consume(wt) + + val s: Int + select { + s = 42 + Job().apply { complete() }.onJoin {} + } + consume(s) + } + + private fun consume(a: Int) { + a.hashCode() // BE codegen verification + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/Builders.kt b/kotlinx-coroutines-core/jvm/src/Builders.kt index b8a250fef6..e4504ccdd4 100644 --- a/kotlinx-coroutines-core/jvm/src/Builders.kt +++ b/kotlinx-coroutines-core/jvm/src/Builders.kt @@ -4,10 +4,12 @@ @file:JvmMultifileClass @file:JvmName("BuildersKt") +@file:OptIn(ExperimentalContracts::class) package kotlinx.coroutines import java.util.concurrent.locks.* +import kotlin.contracts.* import kotlin.coroutines.* /** @@ -34,6 +36,9 @@ import kotlin.coroutines.* */ @Throws(InterruptedException::class) public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } val currentThread = Thread.currentThread() val contextInterceptor = context[ContinuationInterceptor] val eventLoop: EventLoop? diff --git a/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt b/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt index d21a9f895b..e20362ff93 100644 --- a/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt +++ b/kotlinx-coroutines-core/jvm/test/RunBlockingTest.kt @@ -162,4 +162,13 @@ class RunBlockingTest : TestBase() { handle.dispose() } + + @Test + fun testContract() { + val rb: Int + runBlocking { + rb = 42 + } + rb.hashCode() // unused + } }