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 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
|
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
|
JVM, there are guaranties of backward compatibility, which the compiler shall obey in both so-called "Old JVM" back-end, and in the new
|
||||||
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
|
JVM_IR one. The new back-end's naming can differ from the official documentation: the document uses the "IR" suffix, while the official
|
||||||
official documentation omits it.
|
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,"
|
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
|
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.
|
||||||
is no such prefix, the section explains the general behavior of coroutines and shall apply to all back-ends.
|
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
|
The document sticks to release coroutines since we deprecated experimental coroutines in 1.3, and JVM_IR does not support them. However,
|
||||||
not support them. However, there are sections, which explain differences in code generation between release and experimental coroutines
|
some sections explain differences in code generation between release and experimental coroutines wherever appropriate, since we technically
|
||||||
wherever appropriate, since we technically still support them. Sections, which describe experimental coroutines, have a "1.2" prefix.
|
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"
|
If the current implementation is not ideal (or has a bug), there is a description of the difference, and the steps to implement the
|
||||||
version. These subsections start with "FIXME."
|
"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
|
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,
|
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."
|
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
|
The document often uses the term "undefined behavior," which means that we consciously rejected defining it. Thus, the behavior may vary
|
||||||
may vary from version to version, from back-end to back-end, and one should use it with extreme caution.
|
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
|
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.
|
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.
|
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
|
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,
|
example, both `dummy` and `println` are used only inside the lambda. Because one cannot call suspendable functions from ordinary, we can
|
||||||
we can imagine two worlds: suspendable and ordinary. Alternatively, one can consider them as being of two different colors, and we color the
|
imagine two worlds: suspendable and ordinary. Alternatively, one can consider them as two different colors, and we color the program by
|
||||||
program by using the "suspend" modifier.
|
using the "suspend" modifier.
|
||||||
|
|
||||||
The lambda, creatively named `lambda`, contains two suspend calls (`dummy`) and one from the `main` function to the lambda itself,
|
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:
|
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,
|
A separate section explains the exact mechanism of starting a coroutine (in a broad sense) and writing their builders. For now, consider
|
||||||
consider `builder` as a boilerplate to cross the worlds.
|
`builder` as a boilerplate to cross the worlds.
|
||||||
|
|
||||||
Now, when we change `main` to use the builder and not suspend itself
|
Now, when we change `main` to use the builder and not suspend itself
|
||||||
```kotlin
|
```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.
|
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,
|
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
|
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
|
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.
|
FIXME: I should rename `CoroutineTransformerMethodVisitor` to `StateMachineBuilder` already.
|
||||||
|
|
||||||
#### JS & Native: Suspend Markers
|
#### 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
|
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. They still collect suspension points, however.
|
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
|
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
|
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
|
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.
|
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
|
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
|
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
|
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
|
something like `call/cc`. Every coroutine already has a continuation. The caller passes it to the callee as an argument. Since the coroutine
|
||||||
coroutine passes the return value to `resumeWith`, its parameter has the same type as the return type of the coroutine. Technically, the
|
passes the return value to `resumeWith`, its parameter has the same type as the coroutine's return type. Technically, the type is
|
||||||
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
|
`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
|
||||||
`Unit` to illustrate how to resume a coroutine with a value. The other part, `Throwable`, is for resuming a coroutine with an exception
|
illustrate how to resume a coroutine with a value. The other part, `Throwable`, is for resuming a coroutine with an exception and is
|
||||||
and is explained in the relevant section.
|
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
|
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
|
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,
|
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`
|
`suspendMe`'s continuation has type `Continuation<Int>`, i.e., the compiler moves function's return to type argument of the `Continuation`
|
||||||
interface, as I mentioned in the previous section (about continuation-passing style).
|
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`
|
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:
|
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
|
```kotlin
|
||||||
suspendCoroutine<Int> { it.resume(42) }
|
suspendCoroutine<Int> { it.resume(42) }
|
||||||
```
|
```
|
||||||
Following the resume process, `resume` calls continuation's `resumeWith`, which calls `invokeSuspend` with
|
Following the resume process, `resume` calls continuation's `resumeWith`, which calls `invokeSuspend` with value `42`. `$result` then
|
||||||
value `42`. Then, this will be `$result` and work the same as if `suspendMe` returned `42`. In other words, `suspendCoroutine` with an
|
contains the value and work the same as if `suspendMe` returned `42`. In other words, `suspendCoroutine` with an unconditional resume will
|
||||||
unconditional resume will not suspend the coroutine and is semantically the same as returning the value.
|
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.
|
||||||
|
|
||||||
@@ -487,9 +487,9 @@ fun main() {
|
|||||||
c?.resumeWithException(IllegalStateException("BOO"))
|
c?.resumeWithException(IllegalStateException("BOO"))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
which, upon running, will print the exception. Note, that it is printed inside the `builder` function (because of
|
which, upon running, will print the exception. Note, that it is printed inside the `builder` function
|
||||||
`println(result.exceptionOrNull())`). There are a couple of things happening here: one is inside the generated
|
(because of `println(result.exceptionOrNull())`). There are a couple of things happening here: inside the generated state machine and inside
|
||||||
state machine, and the other is inside `BaseContinuationImpl`'s `resumeWith`.
|
`BaseContinuationImpl`'s `resumeWith`.
|
||||||
|
|
||||||
First, we change the generated state machine. As explained before, the type of `$result` variable is `Int | COROUTINE_SUSPENDED |
|
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$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)
|
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
|
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.
|
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.
|
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.
|
collect the object.
|
||||||
|
|
||||||
#### Spilled Variables Naming
|
#### 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.
|
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
|
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 for the variable. We can ease this restriction and assume the variable to be alive until the following suspension point.
|
LVT record. We can ease this restriction and assume the variable to be alive until the following suspension point.
|
||||||
|
|
||||||
#### Stack spilling
|
#### 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
|
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.
|
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
|
`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.
|
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
|
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
|
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
|
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
|
versions of the function: to start a suspend lambda without parameters and to start a coroutine with either one parameter or a receiver. It
|
||||||
parameter or a receiver. It is defined as follows:
|
is defined as follows:
|
||||||
```kotlin
|
```kotlin
|
||||||
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
|
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
|
||||||
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
|
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
|
||||||
@@ -1010,16 +1010,15 @@ So, it
|
|||||||
2. intercepts it
|
2. intercepts it
|
||||||
3. starts it
|
3. starts it
|
||||||
|
|
||||||
Once again, `createCoroutineUnintercepted` has two versions - one without parameters and the other one with exactly one parameter. All it
|
Once again, `createCoroutineUnintercepted` has two versions - without parameters and with exactly one parameter. All it does is calling
|
||||||
does is calling suspending lambda's `create` function. After the interception, we resume the coroutine with a dummy value. As I explained
|
suspending lambda's `create` function. After the interception, we resume the coroutine with a dummy value. As explained in the resume with
|
||||||
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
|
the value section, the state-machine ignores its first state value. Thus, it is the perfect way to start a coroutine without calling
|
||||||
coroutine without calling `invokeSuspend`. However, the way we start callable references is different. Since they are tail-call, in other
|
`invokeSuspend`. However, the way we start callable references is different. Since they are tail-call, in other words, do not have
|
||||||
words, do not have a
|
a continuation inside an object, we wrap them in a hand-written one.
|
||||||
continuation inside an object, we wrap them in a hand-written one.
|
|
||||||
|
|
||||||
#### create
|
#### 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
|
1. creates a copy of the lambda by calling a constructor with captured variables
|
||||||
2. puts `create`'s arguments into parameter fields.
|
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.
|
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
|
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
|
`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.
|
for functions with zero on one parameter.
|
||||||
|
|
||||||
##### Lambda Parameters
|
##### Lambda Parameters
|
||||||
|
|
||||||
We need to put the arguments of the suspend lambda into fields since there can be only one argument of `invokeSuspend` - `$result`.
|
We need to put the suspend lambda arguments into fields since there can be only one argument of `invokeSuspend` - `$result`. The compiler
|
||||||
The compiler moves the lambda body into `invokeSuspend`. Thus, `invokeSuspend` does all the computation. We reuse fields for spilled
|
moves the lambda body into `invokeSuspend`. Thus, `invokeSuspend` does all the computation. We reuse fields for spilled variables for
|
||||||
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
|
parameters as well. For example, if we have a lambda with type `suspend Int.(Long, Any) -> Unit`, then `I$0` hold value of extension
|
||||||
extension receiver,' `J$0` - the first argument, `L$1` - the second one.
|
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
|
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.
|
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
|
### kotlin.suspend
|
||||||
|
|
||||||
`suspend` soft keyword cannot be used with lambdas or function expression yet. It is 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
|
||||||
However, writing the type of the variable is quite annoying, so, since 1.3, there is a function `kotlin.suspend`, which
|
of the variable is quite annoying, so, since 1.3, there is a function `kotlin.suspend`, which can precede lambda without parameters and turn
|
||||||
can precede lambda without parameters and turn it into suspend one. Since `suspend` is a soft keyword, it is possible to
|
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
|
||||||
name a function `suspend`. A user can define `suspend` function, which accepts lambdas, but token sequence `suspend {`
|
`suspend`, which accepts lambdas, but the token sequence `suspend {` can only be used with `kotlin.suspend`. That is just for the transition
|
||||||
can only be used with `kotlin.suspend`. That is just for the transition period and while suspend lambdas and function
|
period and while suspend lambdas and function expressions are not supported in the parser.
|
||||||
expressions are not supported in the parser.
|
|
||||||
|
|
||||||
FIXME: Support it in the parser and stop using the hack.
|
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
|
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
|
'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
|
### Start
|
||||||
|
|
||||||
Nevertheless, there is another problem. To properly support the completion chain, we need to create the continuation and store the
|
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.
|
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:
|
The easiest way to do this is to check for the type of continuation parameter. So, the function preamble will look like:
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -1466,7 +1464,7 @@ fun test($completion: Continuation<Unit>): Any? {
|
|||||||
// state machine
|
// 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.
|
see how we resume and set the bit.
|
||||||
|
|
||||||
### Resume
|
### 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.
|
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.
|
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`,
|
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.
|
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.
|
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
|
ARETURN
|
||||||
```
|
```
|
||||||
|
|
||||||
After tail-call optimization the code becomes
|
After tail-call optimization, the code becomes
|
||||||
```text
|
```text
|
||||||
ALOAD 1 // continuation
|
ALOAD 1 // continuation
|
||||||
INVOKESTATIC returnsInt
|
INVOKESTATIC returnsInt
|
||||||
@@ -1762,10 +1760,10 @@ generated by JVM_IR do.
|
|||||||
|
|
||||||
#### Redundant Locals Elimination
|
#### 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
|
As explained in the section about variables spilling, the inliner spills stack before inlining and unspills it after the inlining. That
|
||||||
bunch of repeated ASTORE and ALOAD instructions, which can break tail-call elimination since there can be a sequence of `ASTORE; ALOAD`
|
results in a bunch of repeated ASTORE and ALOAD instructions, which can break tail-call elimination since there can be a sequence of
|
||||||
between the suspension point and ARETURN. This bytecode modification simplifies the chains and enables tail-call optimization for these
|
`ASTORE; ALOAD` between the suspension point and ARETURN. This bytecode modification simplifies the chains and enables tail-call
|
||||||
cases.
|
optimization for these cases.
|
||||||
|
|
||||||
#### Tail-Call Optimization for Functions Returning Unit
|
#### Tail-Call Optimization for Functions Returning Unit
|
||||||
|
|
||||||
@@ -1799,11 +1797,11 @@ POP
|
|||||||
GETSTATIC kotlin/Unit.INSTANCE
|
GETSTATIC kotlin/Unit.INSTANCE
|
||||||
ARETURN
|
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
|
`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,
|
`CoroutineTransformerMethodVisitor` we do not have information about return types of suspending calls, we see all of them as just `Any?`, we
|
||||||
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
|
need to mark calls to functions, returning Unit, with a marker. The marker is similar to suspend markers, but with a different argument:
|
||||||
markers, but with a different argument: `ICONST_2`. So, full bytecode for `tailCall3` function becomes
|
`ICONST_2`. So, full bytecode for the `tailCall3` function becomes
|
||||||
```text
|
```text
|
||||||
INVOKESTATIC InlineMarker.beforeInlineCall
|
INVOKESTATIC InlineMarker.beforeInlineCall
|
||||||
ALOAD 1 // continuation
|
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`,
|
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`.
|
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
|
||||||
When we call `resume` on it, its `resumeWith` calls `invokeSuspend` and passes `Dummy` as `$result`. The value will be on the stack at the
|
it, its `resumeWith` calls `invokeSuspend` and passes `Dummy` as `$result`. The value will be on the stack at the beginning of the last
|
||||||
beginning of the last state inside the state-machine. It would appear that the suspend function returning `Unit` (in this case `generic`)
|
state inside the state-machine. It would appear that the suspend function returning `Unit` (in this case `generic`) returns `Dummy`.
|
||||||
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
|
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.
|
way, we do the same in `callSuspend` and `callSuspendBy` functions.
|
||||||
|
|||||||
Reference in New Issue
Block a user