Document coroutines codegen: inliner part 3: minor grammar fixes
This commit is contained in:
committed by
Ilmir Usmanov
parent
a736d4fef1
commit
7d41451fba
+74
-77
@@ -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<T>`, 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<T>`, 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<Int>`, 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<Int>`, 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<Int> { 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 <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
|
||||
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<Unit>): 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 `(<params>,Ljava/lang/Object;)Ljava/lang/Object;`. `<params>` are erased.
|
||||
10. public final `invoke` of type `(<params>,Lkotlin/coroutines/Continuation;)Ljava/lang/Object;`. `<params>` are unerased.
|
||||
11. public or package-private constructor: `<init>` of type `(<captured-variables>,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.
|
||||
|
||||
Reference in New Issue
Block a user