diff --git a/libraries/stdlib/coroutines/jvm/src/kotlin/coroutines/experimental/migration/CoroutinesMigration.kt b/libraries/stdlib/coroutines/jvm/src/kotlin/coroutines/experimental/migration/CoroutinesMigration.kt new file mode 100644 index 00000000000..e4aa1faf525 --- /dev/null +++ b/libraries/stdlib/coroutines/jvm/src/kotlin/coroutines/experimental/migration/CoroutinesMigration.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package kotlin.coroutines.experimental.migration + +import kotlin.coroutines.experimental.Continuation as ExperimentalContinuation +import kotlin.coroutines.experimental.CoroutineContext as ExperimentalCoroutineContext +import kotlin.coroutines.experimental.AbstractCoroutineContextElement as ExperimentalAbstractCoroutineContextElement +import kotlin.coroutines.experimental.EmptyCoroutineContext as ExperimentalEmptyCoroutineContext +import kotlin.coroutines.experimental.ContinuationInterceptor as ExperimentalContinuationInterceptor +import kotlin.coroutines.experimental.intrinsics.COROUTINE_SUSPENDED as EXPERIMENTAL_COROUTINE_SUSPENDED +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + + +/** + * Adapter to invoke experimental suspending function. + * + * To invoke experimental suspending function `foo(args)` that returns value of some type `Result` + * from Kotlin 1.3 use the following code: + * + * ``` + * invokeExperimentalSuspend { foo(args, it) } + * ``` + */ +@SinceKotlin("1.3") +public suspend inline fun invokeExperimentalSuspend(crossinline invocation: (ExperimentalContinuation) -> Any?): T = + suspendCoroutineUninterceptedOrReturn { continuation -> + val result = invocation(continuation.toExperimentalContinuation()) + @Suppress("UNCHECKED_CAST") + if (result === EXPERIMENTAL_COROUTINE_SUSPENDED) COROUTINE_SUSPENDED else result as T + } + +/** + * Coverts reference to suspending function to experimental suspending function. + */ +@SinceKotlin("1.3") +public fun (suspend () -> T).toExperimentalSuspendFunction(): (ExperimentalContinuation) -> Any? = + ExperimentalSuspendFunction0Migration(this) + +/** + * Coverts reference to experimental suspending function to suspending function. + */ +@SinceKotlin("1.3") +public fun ((ExperimentalContinuation) -> Any?).toSuspendFunction(): suspend () -> T = + (this as? ExperimentalSuspendFunction0Migration)?.function ?: SuspendFunction0Migration(this)::invoke + +/** + * Coverts reference to suspending function with receiver to experimental suspending function. + */ +@SinceKotlin("1.3") +public fun (suspend (R) -> T).toExperimentalSuspendFunction(): (R, ExperimentalContinuation) -> Any? = + ExperimentalSuspendFunction1Migration(this) + +/** + * Coverts reference to experimental suspending function with receiver to suspending function. + */ +@SinceKotlin("1.3") +public fun ((R, ExperimentalContinuation) -> Any?).toSuspendFunction(): suspend (R) -> T = + (this as? ExperimentalSuspendFunction1Migration)?.function ?: SuspendFunction1Migration(this)::invoke + +/** + * Converts [Continuation] to [ExperimentalContinuation]. + */ +@SinceKotlin("1.3") +public fun Continuation.toExperimentalContinuation(): ExperimentalContinuation = + (this as? ContinuationMigration)?.continuation ?: ExperimentalContinuationMigration(this) + +/** + * Converts [ExperimentalContinuation] to [Continuation]. + */ +@SinceKotlin("1.3") +public fun ExperimentalContinuation.toContinuation(): Continuation = + (this as? ExperimentalContinuationMigration)?.continuation ?: ContinuationMigration(this) + +/** + * Converts [CoroutineContext] to [ExperimentalCoroutineContext]. + */ +@SinceKotlin("1.3") +public fun CoroutineContext.toExperimentalCoroutineContext(): ExperimentalCoroutineContext { + val interceptor = get(ContinuationInterceptor.Key) + val migration = get(ContextMigration.Key) + val remainder = minusKey(ContinuationInterceptor.Key).minusKey(ContextMigration.Key) + val original = migration?.context ?: ExperimentalEmptyCoroutineContext + val result = if (remainder === EmptyCoroutineContext) original else original + ExperimentalContextMigration(remainder) + return if (interceptor == null) result else result + interceptor.toExperimentalContinuationInterceptor() +} + +/** + * Converts [ExperimentalCoroutineContext] to [CoroutineContext]. + */ +@SinceKotlin("1.3") +public fun ExperimentalCoroutineContext.toCoroutineContext(): CoroutineContext { + val interceptor = get(ExperimentalContinuationInterceptor.Key) + val migration = get(ExperimentalContextMigration.Key) + val remainder = minusKey(ExperimentalContinuationInterceptor.Key).minusKey(ExperimentalContextMigration.Key) + val original = migration?.context ?: EmptyCoroutineContext + val result = if (remainder === ExperimentalEmptyCoroutineContext) original else original + ContextMigration(remainder) + return if (interceptor == null) result else result + interceptor.toContinuationInterceptor() +} + +/** + * Converts [ContinuationInterceptor] to [ExperimentalContinuationInterceptor]. + */ +@SinceKotlin("1.3") +public fun ContinuationInterceptor.toExperimentalContinuationInterceptor(): ExperimentalContinuationInterceptor = + (this as? ContinuationInterceptorMigration)?.interceptor ?: ExperimentalContinuationInterceptorMigration(this) + +/** + * Converts [ExperimentalContinuationInterceptor] to [ContinuationInterceptor]. + */ +@SinceKotlin("1.3") +public fun ExperimentalContinuationInterceptor.toContinuationInterceptor(): ContinuationInterceptor = + (this as? ExperimentalContinuationInterceptorMigration)?.interceptor ?: ContinuationInterceptorMigration(this) + +// ------------------ converter classes ------------------ +// Their name starts with "Experimental" if they implement the corresponding Experimental interfaces + +private class ExperimentalSuspendFunction0Migration(val function: suspend () -> T) : (ExperimentalContinuation) -> Any? { + override fun invoke(continuation: ExperimentalContinuation): Any? { + val result = function.startCoroutineUninterceptedOrReturn(continuation.toContinuation()) + return if (result === COROUTINE_SUSPENDED) EXPERIMENTAL_COROUTINE_SUSPENDED else result + } +} + +private class SuspendFunction0Migration(val function: (ExperimentalContinuation) -> Any?) { + suspend fun invoke(): T = suspendCoroutineUninterceptedOrReturn { continuation -> + val result = function(continuation.toExperimentalContinuation()) + if (result === EXPERIMENTAL_COROUTINE_SUSPENDED) COROUTINE_SUSPENDED else result + } +} + +private class ExperimentalSuspendFunction1Migration(val function: suspend (R) -> T) : (R, ExperimentalContinuation) -> Any? { + override fun invoke(receiver: R, continuation: ExperimentalContinuation): Any? { + val result = function.startCoroutineUninterceptedOrReturn(receiver, continuation.toContinuation()) + return if (result === COROUTINE_SUSPENDED) EXPERIMENTAL_COROUTINE_SUSPENDED else result + } +} + +private class SuspendFunction1Migration(val function: (R, ExperimentalContinuation) -> Any?) { + suspend fun invoke(receiver: R): T = suspendCoroutineUninterceptedOrReturn { continuation -> + val result = function(receiver, continuation.toExperimentalContinuation()) + if (result === EXPERIMENTAL_COROUTINE_SUSPENDED) COROUTINE_SUSPENDED else result + } +} + +private class ExperimentalContinuationMigration(val continuation: Continuation): ExperimentalContinuation { + override val context = continuation.context.toExperimentalCoroutineContext() + override fun resume(value: T) = continuation.resume(value) + override fun resumeWithException(exception: Throwable) = continuation.resumeWithException(exception) +} + +private class ContinuationMigration(val continuation: ExperimentalContinuation): Continuation { + override val context: CoroutineContext = continuation.context.toCoroutineContext() + override fun resumeWith(result: SuccessOrFailure) { + result + .onSuccess { continuation.resume(it) } + .onFailure { continuation.resumeWithException(it) } + } +} + +private class ExperimentalContextMigration(val context: CoroutineContext): ExperimentalAbstractCoroutineContextElement(Key) { + companion object Key : ExperimentalCoroutineContext.Key +} + +private class ContextMigration(val context: ExperimentalCoroutineContext): AbstractCoroutineContextElement(Key) { + companion object Key : CoroutineContext.Key +} + +private class ExperimentalContinuationInterceptorMigration(val interceptor: ContinuationInterceptor) : ExperimentalContinuationInterceptor { + override val key: ExperimentalCoroutineContext.Key<*> + get() = ExperimentalContinuationInterceptor.Key + + override fun interceptContinuation(continuation: ExperimentalContinuation): ExperimentalContinuation = + interceptor.interceptContinuation(continuation.toContinuation()).toExperimentalContinuation() +} + +private class ContinuationInterceptorMigration(val interceptor: ExperimentalContinuationInterceptor) : ContinuationInterceptor { + override val key: CoroutineContext.Key<*> + get() = ContinuationInterceptor.Key + + override fun interceptContinuation(continuation: Continuation): Continuation = + interceptor.interceptContinuation(continuation.toExperimentalContinuation()).toContinuation() +} + diff --git a/libraries/stdlib/coroutines/jvm/test/CoroutinesMigrationTest.kt b/libraries/stdlib/coroutines/jvm/test/CoroutinesMigrationTest.kt new file mode 100644 index 00000000000..82d4de450ca --- /dev/null +++ b/libraries/stdlib/coroutines/jvm/test/CoroutinesMigrationTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package test.kotlin.coroutines.experimental.migration + +import test.kotlin.coroutines.TestDispatcher +import java.util.concurrent.Semaphore +import kotlin.coroutines.experimental.buildSequence as experimentalBuildSequence +import kotlin.coroutines.experimental.SequenceBuilder as ExperimentalSequenceBuilder +import kotlin.coroutines.experimental.Continuation as ExperimentalContinuation +import kotlin.coroutines.experimental.CoroutineContext as ExperimentalCoroutineContext +import kotlin.coroutines.experimental.AbstractCoroutineContextElement as ExperimentalAbstractCoroutineContextElement +import kotlin.coroutines.experimental.EmptyCoroutineContext as ExperimentalEmptyCoroutineContext +import kotlin.coroutines.experimental.ContinuationInterceptor as ExperimentalContinuationInterceptor +import kotlin.coroutines.experimental.intrinsics.COROUTINE_SUSPENDED as EXPERIMENTAL_COROUTINE_SUSPENDED +import kotlin.coroutines.experimental.migration.* +import kotlin.coroutines.* +import kotlin.coroutines.experimental.startCoroutine +import kotlin.coroutines.jvm.internal.runSuspend +import kotlin.test.* + +/** + * Tests on coroutines migrations utilities. + */ +class CoroutinesMigrationTest { + @Test + fun testContextMigration() { + assertTrue(EmptyCoroutineContext === ExperimentalEmptyCoroutineContext.toCoroutineContext()) + assertTrue(ExperimentalEmptyCoroutineContext === EmptyCoroutineContext.toExperimentalCoroutineContext()) + MyElement().let { e -> + assertTrue(e === e.toExperimentalCoroutineContext().toCoroutineContext()) + val ee = MyExperimentalElement() + val ctx = (e.toExperimentalCoroutineContext() + ee).toCoroutineContext() + assertTrue(e === ctx[MyElement.Key]) + assertTrue(ee === ctx.toExperimentalCoroutineContext()[MyExperimentalElement.Key]) + } + MyExperimentalElement().let { ee -> + assertTrue(ee === ee.toCoroutineContext().toExperimentalCoroutineContext()) + val e = MyElement() + val ctx = (ee.toCoroutineContext() + e).toExperimentalCoroutineContext() + assertTrue(ee === ctx[MyExperimentalElement.Key]) + assertTrue(e === ctx.toCoroutineContext()[MyElement.Key]) + } + } + + @Test + fun testFunctionMigration() { + var fooCnt = 0 + suspend fun foo() { + fooCnt++ + } + runSuspend { + foo() + } + assertEquals(1, fooCnt) + val foo2 = ::foo.toExperimentalSuspendFunction().toSuspendFunction() + runSuspend { + foo2() + } + assertEquals(2, fooCnt) + var barCnt = 0 + suspend fun bar(x: Int) { + barCnt += x + } + runSuspend { + bar(2) + } + assertEquals(2, barCnt) + val bar2 = ::bar.toExperimentalSuspendFunction().toSuspendFunction() + runSuspend { + bar2(3) + } + assertEquals(5, barCnt) + } + + @Test + fun testInvokeExperimental0() { + testInvokeExperimentalImpl(false) + } + + @Test + fun testInvokeExperimental1() { + testInvokeExperimentalImpl(true) + } + + private fun testInvokeExperimentalImpl(shallSuspend: Boolean) { + TestDispatcher("testInvokeExperimental").use { dispatcher -> + val semaphore = Semaphore(0) + val foo: suspend (Int) -> String = { + dispatcher.assertThread() + if (shallSuspend) { + dispatcher.yield() + } + "OK$it" + } + val fooExp = foo.toExperimentalSuspendFunction() + val testCode: suspend () -> String = { + dispatcher.assertThread() + val result = invokeExperimentalSuspend { + fooExp(42, it) + } + assertEquals("OK42", result) + dispatcher.assertThread() + "DONE" + } + testCode.startCoroutine(Continuation(dispatcher) { result -> + dispatcher.assertThread() + // todo: below does not work due to a bug in inline classes +// assertEquals("DONE", result.getOrThrow()) + semaphore.release() + }) + semaphore.acquire() + } + } + + @Test + fun testExperimentalBuildSequence() { + suspend fun action(builder: ExperimentalSequenceBuilder) { + invokeExperimentalSuspend { builder.yield(1, it) } + invokeExperimentalSuspend { builder.yield(2, it) } + invokeExperimentalSuspend { builder.yield(3, it) } + } + val seq = experimentalBuildSequence(::action.toExperimentalSuspendFunction()) + assertEquals(listOf(1, 2, 3), seq.toList()) + } + + class MyElement : AbstractCoroutineContextElement(Key) { + companion object Key : CoroutineContext.Key + } + + class MyExperimentalElement : ExperimentalAbstractCoroutineContextElement(Key) { + companion object Key : ExperimentalCoroutineContext.Key + } +} + diff --git a/libraries/stdlib/coroutines/jvm/test/CoroutinesTest.kt b/libraries/stdlib/coroutines/jvm/test/CoroutinesTest.kt index 3861257d154..dbe74bcb965 100644 --- a/libraries/stdlib/coroutines/jvm/test/CoroutinesTest.kt +++ b/libraries/stdlib/coroutines/jvm/test/CoroutinesTest.kt @@ -52,35 +52,3 @@ class DispatcherSwitcher( } } } - -class TestDispatcher( - private val name: String -) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor, Closeable { - private lateinit var thread: Thread - - val executor: ExecutorService = Executors.newSingleThreadExecutor { runnable -> - Thread(runnable, name).also { thread = it } - } - - fun assertThread() { - assertEquals(thread, Thread.currentThread()) - } - - override fun interceptContinuation(continuation: Continuation): Continuation = - DispatchedContinuation(continuation) - - override fun close() { - executor.shutdown() - } - - inner class DispatchedContinuation(val delegate: Continuation) : Continuation { - override val context: CoroutineContext = delegate.context - - override fun resumeWith(result: SuccessOrFailure) { - executor.execute { - delegate.resumeWith(result) - } - } - } -} - diff --git a/libraries/stdlib/coroutines/jvm/test/TestDispatcher.kt b/libraries/stdlib/coroutines/jvm/test/TestDispatcher.kt new file mode 100644 index 00000000000..5b2f5d481ef --- /dev/null +++ b/libraries/stdlib/coroutines/jvm/test/TestDispatcher.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package test.kotlin.coroutines + +import java.io.Closeable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.coroutines.* +import kotlin.test.assertEquals + +class TestDispatcher( + private val name: String +) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor, Closeable { + private lateinit var thread: Thread + + val executor: ExecutorService = Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, name).also { thread = it } + } + + fun assertThread() { + assertEquals(thread, Thread.currentThread()) + } + + override fun interceptContinuation(continuation: Continuation): Continuation = + DispatchedContinuation(continuation) + + override fun close() { + executor.shutdown() + } + + suspend fun yield() = suspendCoroutine { cont -> + executor.execute { + assertThread() + cont.resume(Unit) + } + } + + inner class DispatchedContinuation(val delegate: Continuation) : Continuation { + override val context: CoroutineContext = delegate.context + + override fun resumeWith(result: SuccessOrFailure) { + executor.execute { + delegate.resumeWith(result) + } + } + } +}