From 8b604b8ce1cc2c2defc73f23e839c7b1ec068509 Mon Sep 17 00:00:00 2001 From: Ilmir Usmanov Date: Sun, 16 Aug 2020 00:35:06 +0200 Subject: [PATCH] Document coroutines codegen: intrinsics part 2 --- .../codegen/coroutines/coroutines-codegen.md | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md index 4672b7c6238..63fa79348d6 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md @@ -939,3 +939,125 @@ suspend fun main() { alsoReturnsInt() } ``` + +#### SafeContinuation + +Of course, there is a reason for `SafeContinuation`. Let's consider the following example: +```kotlin +fun builder(c: suspend () -> Unit) { + c.startCoroutine(object: Continuation { + override val context = EmptyCoroutineContext + override fun resumeWith(result: Result) { + result.getOrThrow() + } + }) +} + +fun main() { + builder { + suspendCoroutineUninterceptedOrReturn { + it.resumeWithException(IllegalStateException("Boo")) + } + } +} +``` +One might assume, that we will get `IllegalStateException`, but this in not what happens here: +```text +Exception in thread "main" kotlin.KotlinNullPointerException + at kotlin.coroutines.jvm.internal.ContinuationImpl.releaseIntercepted(ContinuationImpl.kt:118) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:39) + at kotlin.coroutines.ContinuationKt.startCoroutine(Continuation.kt:114) +``` +That is an example of undefined behavior. + +So, what happens here and why it causes the KNPE? When we call `resumeWithException`, inside `BaseContinuationImpl.resumeWith` we call +`releaseIntercepted`, where we set `intercepted` field to `CompletedContinuation`: +```kotlin +protected override fun releaseIntercepted() { + val intercepted = intercepted + if (intercepted != null && intercepted !== this) { + context[ContinuationInterceptor]!!.releaseInterceptedContinuation(intercepted) + } + this.intercepted = CompletedContinuation // just in case +} +``` +Then, when we throw the exception by calling `getOrThrow`, `BaseContinuationImpl.resumeWith` catches it (see the section about resume with +exception), and calls `releaseIntercepted` again, but since there is no continuation interceptor in `context`, we get the KNPE. + +That is what essentially `SafeContinuation` prevents. It catches an exception inside its `resumeWith` method and saves it until +`suspendCoroutine` calls `getOrThrow`. Also, `getOrThrow` returns `COROUTINE_SUSPENDED` for not-yet-finished coroutines. In other words, +when a wrapped coroutine suspends, `getOrThrow` tells `suspendCoroutine` to suspend. + +### startCoroutine + +We have already covered how a coroutine suspends, what happens when it resumes and how the compiler handles it. However, we have never +looked at how one can create or start a coroutine. In all previous examples, one could notice a call to `startCoroutine`. There are two +versions of the function: one is to start a suspend lambda without parameters and the other one - to start a coroutine with either one +parameter or a receiver. It is defined as follows: +```kotlin +public fun (suspend () -> T).startCoroutine(completion: Continuation) { + createCoroutineUnintercepted(completion).intercepted().resume(Unit) +} +``` +So, it +1. creates a coroutine +2. intercepts it +3. starts it + +Once again, `createCoroutineUnintercepted` has two versions - one without parameters and the other one with exactly one parameter. All it +does is calling suspending lambda's `create` function. After the interception, we resume the coroutine with a dummy value. As I explained +in the resume with the value section, the state-machine ignores the value in its first state. Thus, it is the perfect way to start a +coroutine without calling `invokeSuspend`. However, the way we start callable references is different. Since they are tail-call, in other +words, do not have a +continuation inside an object, we wrap them in a hand-written one. + +#### create + +`create` is generated by the compiler and it +1. creates a copy of the lambda by calling a constructor with captured variables +2. puts `create`'s arguments into parameter fields. + +For example, if we have a lambda like +```kotlin +fun main() { + val i = 1 + val lambda: suspend (Int) -> Int = { i + it } +} +``` +the generated `create` will look like +```kotlin +public fun create(value: Any?, completion: Continuation): Continuation { + val resutl = main$lambda$1(this.$i, completion) + result.I$0 = value as Int +} +``` +note that the constructor, in addition to captured parameters, accepts a completion object. + +In Old JVM BE, `create` is generated for every suspend lambda even when we do not need the function. I.e., even for suspending lambdas with +more than one parameter. There are only two versions of `createCoroutineUnintercepted`, and there are no other places where we call +`create` (apart from compiler-generated `invoke`s). Thus, in JVM_IR BE, we fixed the slip-up, and it generates the `create` function only +for functions with zero on one parameter. + +##### Lambda Parameters + +We need to put the arguments of the suspend lambda into fields since there can be only one argument of `invokeSuspend` - `$result`. +The compiler moves the lambda body into `invokeSuspend`. Thus, `invokeSuspend` does all the computation. We reuse fields for spilled +variables for parameters as well. For example, if we have a lambda with type `suspend Int.(Long, Any) -> Unit`, then `I$0` hold value of +extension receiver,' `J$0` - the first argument, `L$1` - the second one. + +This way, we can reuse spilled variables cleanup logic for parameters. If we used separate fields for parameters, we would need to manually +push `null` to them as we do for spilled variable fields if we do not need them anymore. + +#### invoke + +`invoke` is basically `startCoroutine` without an interception. In `invoke`, we call `create` and resume a new instance with dummy value by +calling `invokeSuspend`. We cannot just call `invokeSuspend` without calling the constructor first is that it would not create a +continuation needed for the completion chain, as explained in the continuation-passing style section. Also, recursive suspend lambda calls +would reset `label`'s value. + +FIXME: We do not need to create an additional copy of the lambda if we can verify that we do not pass them as completion to themselves. +However, this includes not only recursive lambdas. We can pass the lambda to a tail-call suspending function and call it there. In this +case, the continuation object is the same, and we have the same problems as if there was a recursion. + +Of course, in JVM_IR, we do not have a `create` function in case when the lambda has more than one parameter, `invoke` creates a new +instance of the lambda with copies of all captured variables and then puts the parameters of the lambda to fields. \ No newline at end of file