Document coroutines codegen: resume with exception

This commit is contained in:
Ilmir Usmanov
2020-08-14 03:23:19 +02:00
committed by Ilmir Usmanov
parent 7e047ac96f
commit 57e69202e8
@@ -449,4 +449,105 @@ Following the resume process, `resume` calls continuation's `resumeWith`, which
value `42`. Then, this will be `$result` and work the same as if `suspendMe` returned `42`. In other words, `suspendCoroutine` with an
unconditional resume will not suspend the coroutine and is semantically the same as returning the value.
It is important to note that passing `COROUTINE_SUSPENDED` to continuation's `resumeWith` leads to undefined behavior.
It is important to note that passing `COROUTINE_SUSPENDED` to continuation's `resumeWith` leads to undefined behavior.
### Resume with Exception
After reading the previous section about resume with a value, one might assume that `$result`'s type is `Int | COROUTINE_SUSPENDED`, but
this is not completely true. It is `Int | COROUTINE_SUSPENDED | Result$Failue(Throwable)`, or, more generally, it is `returnType |
COROUTINE_SUSPENDED | Result$Failue(Throwable)`. The section covers the last part: `Result$Failue(Throwable)`.
Let us change the previous example to resume the coroutine with exception:
```kotlin
import kotlin.coroutines.*
var c: Continuation<Int>? = null
suspend fun suspendMe(): Int = suspendCoroutine { continuation ->
c = continuation
}
fun builder(c: suspend () -> Unit) {
c.startCoroutine(object: Continuation<Unit> {
override val context = EmptyCoroutineContext
override fun resumeWith(result: Result<Unit>) {
println(result.exceptionOrNull())
}
})
}
fun main() {
val a: suspend () -> Unit = { println(1 + suspendMe()) }
builder { a() }
c?.resumeWithException(IllegalStateException("BOO"))
}
```
which, upon running, will print the exception. Note, that it is printed inside the `builder` function (because of
`println(result.exceptionOrNull())`). There are a couple of things happening here: one is inside the generated
state machine, and the other is inside `BaseContinuationImpl`'s `resumeWith`.
First, we change the generated state machine. As explained before, the type of `$result` variable is `Int | COROUTINE_SUSPENDED |
Result$Failue(Throwable)`, but when we resume, by convention, its type cannot be `COROUTINE_SUSPENDED`. Still, the type is `Int |
Result$Failure(Throwable)`, which we cannot just pass to `plus`, at least, without a check and `CHECKCAST`. Otherwise, we will get CCE at
runtime. Thus, we check the `$result` variable and throw the exception if the variable holds it.
```kotlin
fun invokeSuspend($result: Any?): Any? {
when(this.label) {
0 -> {
this.label = 1
$result = suspendMe(this)
if ($result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
goto 1
}
1 -> {
$result.throwOnFailure()
println(1 + $result)
return Unit
}
else -> {
throw IllegalStateException("call to 'resume' before 'invoke' with coroutine")
}
}
}
```
where `throwOnFailure` is a function that performs the check and throwing part for us.
Now, when we throw the exception, it should end up in the `main` function. However, as we saw from the example, it comes to `builder`'s
root continuation's `resumeWith`. The builder creates the root continuation, and, unlike other continuations, it has no completion. We
expect it to reach root continuation, since when we call one suspending function or lambda from another, we want to propagate the exception
through suspending stack (also known as an async stack), from callee to caller, regardless of whether or not there was a suspension, unless,
there is no explicit try-catch block. Thankfully, we can propagate the exception the same way as the execution upon coroutine's completion,
through the chain of `completion` fields. We, after all, should pass it to the caller, just like the return value. When `invokeSuspend`
throws an exception, `BaseContinuationImpl.resumeWith` catches it, wraps into `Result` inline class, which is essentially `T |
Result$Failure(Throwable)`, and calls `completion`'s `resumeWith` with the result (simplified):
```kotlin
abstract class BaseContinuationImpl(
private val completion: Continuation<Any?>
): Continuation<Any?> {
public final override fun resumeWith(result: Result<Any?>) {
val outcome = try {
val outcome = invokeSuspend(result)
if (outcome == COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (e: Throwable) {
Result.failure(e)
}
completion.resumeWith(outcome)
}
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}
```
The function passes the exception to `invokeSuspend`, `invokeSuspend` calls `throwOnFailure` and throws it again, then the exception is
caught in `BaseContinuationImpl.resumeWith` and wrapped again until it reaches root continuation's `resumeWith`, where, in this case, the
coroutine builder prints it. By the way, `resumeWithException` works in release coroutines precisely in the same way (except the catching
part): it wraps the exception into `Result` like in a burrito. It passes it to continuation's `resumeWith`. `resume` also wraps the argument
into `Result` and passes it to `resumeWith`.
#### 1.2: Data and Exception
Since 1.3 introduced inline classes and `Result` as one of them, experimental coroutines use a different approach to passing value and
exception to `doResume`, which was the name of `invokeSuspend` in experimental coroutines.
Instead of one parameter with type `returnType | COROUTINE_SUSPENDED | Result$Failure(Throwable)`, experimental
coroutines' suspend lambda's `doResume` accepts two parameters: `data` and `exception`. `data` has type `returnType | COROUTINE_SUSPENDED`
and `exception` has type `Throwable`. `resume` and `resumeWithException` used to be methods of `Continuation` interface and in 1.3 they
were replaced by `resumeWith`. `resume` and `resumeWithException` are now extension functions on `Continuation`, which call `resumeWith`.