Document coroutines codegen: intrinsics part 2

This commit is contained in:
Ilmir Usmanov
2020-08-16 00:35:06 +02:00
committed by Ilmir Usmanov
parent 611c1b1a38
commit 8b604b8ce1
@@ -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<Unit> {
override val context = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
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 <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
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.