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 bd85ae5e074..faa3961ed83 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 @@ -6,27 +6,27 @@ importantly, why the compiler behaves like this (or, to be precise, should behav the compiler and advanced Kotlin programmers to understand the reasons behind specific design decisions. The document is JVM-centric, that means it explains how things work in JVM BE since this is the area I am most familiar with and since in -JVM, there are guaranties of backward compatibility, which the compiler shall obey in both so-called "Old JVM" back-end, as well as in the -new JVM_IR one. The naming of the new back-end can differ from the official documentation: the document uses the "IR" suffix, while the -official documentation omits it. +JVM, there are guaranties of backward compatibility, which the compiler shall obey in both so-called "Old JVM" back-end, and in the new +JVM_IR one. The new back-end's naming can differ from the official documentation: the document uses the "IR" suffix, while the official +documentation omits it. If the name of a section of the document has an "Old JVM:" prefix, it explains old JVM back-end specific details; if the prefix is "JVM_IR," -then it is JVM_IR back-end specific. If the prefix is plain "JVM," the explanation applies to both the old back-end and the new one. If there -is no such prefix, the section explains the general behavior of coroutines and shall apply to all back-ends. +then it is JVM_IR back-end specific. If the prefix is common "JVM," the explanation applies to both the old back-end and the new one. +Otherwise, the section explains the general behavior of coroutines and shall apply to all back-ends. -The document sticks to release coroutines since we deprecated experimental coroutines in 1.3, and JVM_IR does -not support them. However, there are sections, which explain differences in code generation between release and experimental coroutines -wherever appropriate, since we technically still support them. Sections, which describe experimental coroutines, have a "1.2" prefix. +The document sticks to release coroutines since we deprecated experimental coroutines in 1.3, and JVM_IR does not support them. However, +some sections explain differences in code generation between release and experimental coroutines wherever appropriate, since we technically +still support them. Sections, which describe experimental coroutines, have a "1.2" prefix. -If the current implementation is not ideal (or has a bug), there is a description of the difference and the steps to implement the "correct" -version. These subsections start with "FIXME." +If the current implementation is not ideal (or has a bug), there is a description of the difference, and the steps to implement the +"correct" version. These subsections start with "FIXME." Throughout the document term "coroutine" will represent either a suspend lambda or a suspend function, which is different from the usual definition of coroutines - something like a lightweight thread. The document reuses the term since "suspend lambda or function" is wordy, and when it requires the typical definition, it says explicitly "a coroutine in a broad sense." -The document often uses the term "undefined behavior," which means that we consciously rejected defining the behavior. Thus, the behavior -may vary from version to version, from back-end to back-end, and one should use it with extreme caution. +The document often uses the term "undefined behavior," which means that we consciously rejected defining it. Thus, the behavior may vary +from version to version, from back-end to back-end, and one should use it with extreme caution. Lastly, most of the examples presented in the document actually suspend, so one is sure every piece is in place since coroutines is a broad and complex topic, and it is easy to forget one piece, which will lead to a runtime error or even worse, semantically wrong code execution. @@ -52,9 +52,9 @@ suspend fun main() { which, upon running, will print `1` and `2`, as expected. One can call a suspend function only from other suspend function or suspend lambda, but it can call ordinary, non-suspendable functions. For -example, both `dummy` and `println` are used only inside the lambda. Because one is not allowed to call suspendable functions from ordinary, -we can imagine two worlds: suspendable and ordinary. Alternatively, one can consider them as being of two different colors, and we color the -program by using the "suspend" modifier. +example, both `dummy` and `println` are used only inside the lambda. Because one cannot call suspendable functions from ordinary, we can +imagine two worlds: suspendable and ordinary. Alternatively, one can consider them as two different colors, and we color the program by +using the "suspend" modifier. The lambda, creatively named `lambda`, contains two suspend calls (`dummy`) and one from the `main` function to the lambda itself, but there is no suspension. Let us add it: @@ -98,8 +98,8 @@ fun builder(c: suspend () -> Unit) { }) } ``` -A separate section explains the exact mechanism of starting a coroutine (in a broad sense) and how one can write their builders. For now, -consider `builder` as a boilerplate to cross the worlds. +A separate section explains the exact mechanism of starting a coroutine (in a broad sense) and writing their builders. For now, consider +`builder` as a boilerplate to cross the worlds. Now, when we change `main` to use the builder and not suspend itself ```kotlin @@ -254,7 +254,7 @@ is turned into a coroutine, which is, by definition, is a suspendable unit of co That is the reason why we need to turn linear code into a state machine. -On a closing note, the state machine should be flat; in other words, there should be no state-machine inside the state of a state-machine. +On a closing note, the state machine should be flat; in other words, there should be no state-machine inside a state-machine state. Otherwise, inner state-machine states will rewrite `label`, breaking the whole suspend-resume machinery and leading to weird behavior, ranging from CCE to infinite loops. Similar buggy behavior happens when several suspending calls are in one state: when the first call suspends, and then the execution resumes, skipping all the remaining code in the state. Both these bugs were quite frequent in the early @@ -283,8 +283,8 @@ points by checking these markers, and then it generates the state-machine. FIXME: I should rename `CoroutineTransformerMethodVisitor` to `StateMachineBuilder` already. #### JS & Native: Suspend Markers -The difference between JVM_IR and JS_IR/Native in regards to coroutine codegen non-JVM back-ends do not generate suspending markers in the -resulting code. They still collect suspension points, however. +The difference between JVM_IR and JS_IR/Native regarding coroutine codegen non-JVM back-ends do not generate suspending markers in the +resulting code. That is because they assume the closed-world model; in other words, they do not generate libraries in their target languages, which could contain suspending inline functions. Thus, the back-ends run the inliner before all lowerings, and they generate state machine during a suspend lowering, whereas JVM_IR still relies on the old back-end's `CoroutineTransfromerMethodVisitor` to do this since we cannot just @@ -352,14 +352,14 @@ The type parameter is the same as the type parameter of section, where we create a continuation object. The object overrides `resumeWith` with the same signature. Adding the `continuation` parameter to suspend lambdas and functions is known as Continuation-Passing Style, the style actively used in -lisps. For example, in Scheme, if a function returns a value in a continuation-passing style, it passes the value to the continuation +lisps. For example, if a function returns a value in a continuation-passing style in Scheme, it passes the value to the continuation parameter. So, a function accepts the continuation parameter, and the caller passes the continuation by calling `call/cc` intrinsic. The - same happens in Kotlin with passing return value to caller's continuation's `resumeWith`. However, unlike Scheme, Kotlin does not use - something like `call/cc`. Every coroutine already has a continuation. The caller passes it to the callee as an argument. Since the - coroutine passes the return value to `resumeWith`, its parameter has the same type as the return type of the coroutine. Technically, the - type is `Result`, but it is just a union `T | Throwable`; in this case, `T` is `Unit`. The next section uses return types other than - `Unit` to illustrate how to resume a coroutine with a value. The other part, `Throwable`, is for resuming a coroutine with an exception - and is explained in the relevant section. +same happens in Kotlin with passing return value to caller's continuation's `resumeWith`. However, unlike Scheme, Kotlin does not use +something like `call/cc`. Every coroutine already has a continuation. The caller passes it to the callee as an argument. Since the coroutine +passes the return value to `resumeWith`, its parameter has the same type as the coroutine's return type. Technically, the type is +`Result`, but it is just a union `T | Throwable`; in this case, `T` is `Unit`. The next section uses return types other than `Unit` to +illustrate how to resume a coroutine with a value. The other part, `Throwable`, is for resuming a coroutine with an exception and is +explained in the relevant section. After we passed parent coroutine's continuation to a child coroutine, we need to store it somewhere. Since "parent coroutine's continuation" is quite long and mouthful for a name, we call it 'completion'. We chose this name because the coroutine calls it upon the @@ -417,8 +417,8 @@ fun main() { } ``` if one runs the program, it prints `42`. However, `suspendMe` does not return `42`. It just suspends and returns nothing. By the way, -`suspendMe`'s continuation has type `Continuation`, i.e., the return type of the function is used as a type argument of `Continuation` -interface, as I mentioned in the previous section (about continuation-passing style). +`suspendMe`'s continuation has type `Continuation`, i.e., the compiler moves function's return to type argument of the `Continuation` +interface I mentioned in the previous section (about continuation-passing style). The state-machine section touched upon the `$result` variable inside the `invokeSuspend` function. The listing shows the `invokeSuspend` function of `a`, but, unlike the previous example, with its signature: @@ -451,9 +451,9 @@ So, what happens, when we call `resume` inside `suspendCoroutine`? Like ```kotlin suspendCoroutine { it.resume(42) } ``` -Following the resume process, `resume` calls continuation's `resumeWith`, which calls `invokeSuspend` with -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. +Following the resume process, `resume` calls continuation's `resumeWith`, which calls `invokeSuspend` with value `42`. `$result` then +contains the value 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. @@ -487,9 +487,9 @@ fun main() { 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`. +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: inside the generated state machine and 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 | @@ -601,7 +601,7 @@ fun main() { c?.resume(Unit) } ``` -here, we should save `a1` before `suspendMe`, and we should restore it after the resumption. Similarly, we should save both `a1` and `a2` +We should save `a1` before `suspendMe`, and we should restore it after the resumption. Similarly, we should save both `a1` and `a2` before `+`, since the compiler does not generally know whether suspend call will suspend, so it assumes that the suspension might happen in each suspension point. So, it spills the locals before each call and unspills after it. @@ -641,7 +641,7 @@ fun invokeSuspend($result: Any?): Any? { ``` As one can see, the generated code does not spill and unspill variables, which are dead, in other words, which are not required afterward. -Furthermore, it cleans the field for spilled variables of reference types up to avoid memory leaks by pushing `null` to it so that GC can +Furthermore, it cleans the field for spilled variables of reference types to avoid memory leaks by pushing `null` to it so that GC can collect the object. #### Spilled Variables Naming @@ -687,8 +687,8 @@ nullify all of them every suspension point. Instead, the compiler checks which o Additionally, the compiler shrinks and splits LVT records for local variables, so a debugger will not show dead variables as uninitialized. -FIXME: Currently, dead variables do not present in LVT. So, if a programmer defines a variable but does not use it, the compiler removes the -LVT record for the variable. We can ease this restriction and assume the variable to be alive until the following suspension point. +FIXME: Currently, dead variables do not present in LVT. So, if a programmer defines a variable but does not use it, the compiler removes its +LVT record. We can ease this restriction and assume the variable to be alive until the following suspension point. #### Stack spilling In the previous examples, the stack was clean before a call, meaning that there were only call arguments before the call, and only the call @@ -911,7 +911,7 @@ So, it does five different things: First, let us examine how one can access a continuation argument without suspending current executions. `suspendCoroutineUninterceptedOrReturn` is an intrinsic function that does only one thing: inlines provided lambda parameter passing -continuation parameter to it. Since its purpose is to give access to the continuation argument, which is invisible in suspend functions +continuation parameter to it. Its purpose is to give access to the continuation argument, which is invisible in suspend functions and lambdas. Thus we cannot write in pure Kotlin. It has to be intrinsic. Fun fact: since the lambda returns `returnType | COROUTINE_SUSPENDED`, the compiler does not check its return type, so there can be some @@ -998,8 +998,8 @@ when a wrapped coroutine suspends, `getOrThrow` tells `suspendCoroutine` to susp 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: +versions of the function: to start a suspend lambda without parameters and 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) @@ -1010,16 +1010,15 @@ So, it 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. +Once again, `createCoroutineUnintercepted` has two versions - without parameters and 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 explained in the resume with +the value section, the state-machine ignores its first state value. 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 +`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. @@ -1039,17 +1038,17 @@ public fun create(value: Any?, completion: Continuation): Continuation { ``` 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 +In Old JVM BE, `create` is generated for every suspend lambda even when we do not need it. 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. +We need to put the suspend lambda arguments 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. @@ -1362,12 +1361,11 @@ The compiler knows the arity, but the completion is provided as an argument to t ### kotlin.suspend -`suspend` soft keyword cannot be used with lambdas or function expression yet. It is not supported in the parser. -However, writing the type of the variable is quite annoying, so, since 1.3, there is a function `kotlin.suspend`, which -can precede lambda without parameters and turn it into suspend one. Since `suspend` is a soft keyword, it is possible to -name a function `suspend`. A user can define `suspend` function, which accepts lambdas, but token sequence `suspend {` -can only be used with `kotlin.suspend`. That is just for the transition period and while suspend lambdas and function -expressions are not supported in the parser. +`suspend` soft keyword cannot be used with lambdas or function expression yet. It is not supported in the parser. However, writing the type +of the variable is quite annoying, so, since 1.3, there is a function `kotlin.suspend`, which can precede lambda without parameters and turn +it into suspend one. Since `suspend` is a soft keyword, it is possible to name a function `suspend`. A user can define a function named +`suspend`, which accepts lambdas, but the token sequence `suspend {` can only be used with `kotlin.suspend`. That is just for the transition +period and while suspend lambdas and function expressions are not supported in the parser. FIXME: Support it in the parser and stop using the hack. @@ -1429,12 +1427,12 @@ suspendFun5 ``` thus, instead of moving the function body to lambda, we keep it in the function and build the state-machine there. However, we also keep the 'lambda', so we store all spilled variables there and the label. This 'lambda' is called continuation, and it is, essentially, the state of -the coroutine. So, unlike suspend lambdas, we split the state (and call it continuation) and the state-machine for suspending functions. +the coroutine. Unlike suspend lambdas, we split the state (and call it continuation) and the state-machine for suspending functions. ### Start Nevertheless, there is another problem. To properly support the completion chain, we need to create the continuation and store the -continuation parameter in the `completion` field. Also, we need to support resuming the coroutine, i.e., we need to get `label` and spilled +continuation parameter in the `completion` field. We also need to support resuming the coroutine, i.e., we need to get `label` and spilled variables from the continuation. So, we need to distinguish these two cases: starting anew and continuing previously suspended execution. The easiest way to do this is to check for the type of continuation parameter. So, the function preamble will look like: ```kotlin @@ -1466,7 +1464,7 @@ fun test($completion: Continuation): Any? { // state machine } ``` -here, we assume that in recursive calls, the sign bit is unset, while the continuation class sets it during the resume process. So, let us +here, we assume that the sign bit is unset in recursive calls, while the continuation class sets it during the resume process. So, let us see how we resume and set the bit. ### Resume @@ -1613,7 +1611,7 @@ It overrides `BaseContinuationImpl`'s `invokeSuspend`. 9. public final `invoke` of type `(,Ljava/lang/Object;)Ljava/lang/Object;`. `` are erased. 10. public final `invoke` of type `(,Lkotlin/coroutines/Continuation;)Ljava/lang/Object;`. `` are unerased. 11. public or package-private constructor: `` of type `(,Lkotlin/coroutines/Continuation;)V`, -where we call `SuspendLambda`'s constructor with arity and completion and initlialize captured variables. +where we call `SuspendLambda`'s constructor with arity and completion and initialize captured variables. As for suspending lambdas, the compiler knows the function's arity, but the completion is provided as an argument to the constructor. FIXME: There is a massive amount of bugs because of this implementation. For example, local suspend functions can hardly be recursive. @@ -1673,7 +1671,7 @@ INVOKESTATIC InlineMarker.afterInlineCall ARETURN ``` -After tail-call optimization the code becomes +After tail-call optimization, the code becomes ```text ALOAD 1 // continuation INVOKESTATIC returnsInt @@ -1762,10 +1760,10 @@ generated by JVM_IR do. #### Redundant Locals Elimination -As explained in the section about variables spilling, the inliner spills stack before inlining and unspills it after. That results in a -bunch of repeated ASTORE and ALOAD instructions, which can break tail-call elimination since there can be a sequence of `ASTORE; ALOAD` -between the suspension point and ARETURN. This bytecode modification simplifies the chains and enables tail-call optimization for these -cases. +As explained in the section about variables spilling, the inliner spills stack before inlining and unspills it after the inlining. That +results in a bunch of repeated ASTORE and ALOAD instructions, which can break tail-call elimination since there can be a sequence of +`ASTORE; ALOAD` between the suspension point and ARETURN. This bytecode modification simplifies the chains and enables tail-call +optimization for these cases. #### Tail-Call Optimization for Functions Returning Unit @@ -1799,11 +1797,11 @@ POP GETSTATIC kotlin/Unit.INSTANCE ARETURN ``` -as one sees, `Unit` is `POP`ed, and then is pushed to the stack and returned. We unfortunately, cannot just remove +as one sees, `Unit` is `POP`ed, and then is pushed to the stack and returned. Unfortunately, we cannot just remove `POP; GETSTATIC kotlin/Unit.INSTANCE`: if we replace `returnsUnit` with `returnsInt`, the bytecode is the same. Since inside -`CoroutineTransformerMethodVisitor` we do not have information about return types of suspending calls, -we see all of them as just `Any?`, we need to mark calls to functions, returning Unit, with a marker. The marker is similar to suspend -markers, but with a different argument: `ICONST_2`. So, full bytecode for `tailCall3` function becomes +`CoroutineTransformerMethodVisitor` we do not have information about return types of suspending calls, we see all of them as just `Any?`, we +need to mark calls to functions, returning Unit, with a marker. The marker is similar to suspend markers, but with a different argument: +`ICONST_2`. So, full bytecode for the `tailCall3` function becomes ```text INVOKESTATIC InlineMarker.beforeInlineCall ALOAD 1 // continuation @@ -1909,10 +1907,9 @@ Let us have a look at the completion chain: +-----------+ ``` That is right; there is only one continuation, generated by the compiler: `main$1`. Moreover, it is passed to `generic` and then to `tx`, -since these functions are tail-call and do not create a continuation themselves. In `tx`, it is saved so that we can resume it in `main`. -When we call `resume` on it, its `resumeWith` calls `invokeSuspend` and passes `Dummy` as `$result`. The value will be on the stack at the -beginning of the last state inside the state-machine. It would appear that the suspend function returning `Unit` (in this case `generic`) -returns `Dummy`. +since these functions are tail-call and do not create a continuation. In `tx`, it is saved to resume it in `main`. When we call `resume` on +it, its `resumeWith` calls `invokeSuspend` and passes `Dummy` as `$result`. The value will be on the stack at the beginning of the last +state inside the state-machine. It would appear that the suspend function returning `Unit` (in this case `generic`) returns `Dummy`. To fix the issue, we generate `POP; GETSTATIC kotlin/Unit.INSTANCE` on the call site, when we are sure that callee returns `Unit`. By the way, we do the same in `callSuspend` and `callSuspendBy` functions.