-
Notifications
You must be signed in to change notification settings - Fork 10.5k
SIL.rst: Add documentation for async function representation. #33994
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Unlike our existing coroutines, async functions run independently within an async coroutine context, and don't directly yield values back and forth. They therefore mostly behave like normal functions with only an `@async` annotation to indicate the presence of async suspend points. The `withUnsafeContinuation` primitive requires some instructions to represent the operation that prepares the continuation to be resumed, which will be represented by `begin_async_continuation`...`await_async_continuation` regions.
Thanks for writing these docs. At the SIL level, explaining async coroutines in terms of @async functions confuses me. Calling an Instead, here we're talking about any function that may suspend execution in some way that does not involve calling an In SIL, there's no reason that a synchronous function can't suspend. I think that will be important to support at the language-level when we can statically determine the executor. There will be some type annotation on such functions and additional type restrictions on its interface similar to the restrictions on async coroutines. So, if we needed some SIL function attribute to guard the use of these instructions, I would call it Regarding the implications for SIL passes, a lot seems to be hidden in this statement:
We won't be able to allow side-effect free code motion across the await, which is something we've never had to content with. Do we need to add a constraint that values defined within the suspend region can't be used outside the suspend region? We also, obviously can't reason about side effects across the await. The basically means that the await appears to write to the entire memory space, meaning we can't optimize anything across suspends. |
Maybe this is a matter of different perspective, whether you consider the "coroutine" to be the notional execution context that persists across async calls, or to be the mechanical LLVM coroutine lowering goop. I guess I'm speaking more in terms of the former than the latter—as I see it, the LLVM coroutine splitting is an implementation detail, whereas the Task that async functions run in is part of the programming model. An async function doesn't represent a whole Task/coroutine unto itself, but a piece of work that can run on that Task.
I guess it isn't clear, but the intent of these instructions was to give something for
Could you give me an example of what you have in mind? As I understand it, anything associated with an executor that might suspend ought to be
Reading this back, I see it sounds like it's implying that resuming a continuation before Thanks for taking a look and giving me your feedback, Andy! |
docs/SIL.rst
Outdated
A coroutine type may declare any number of *yielded values*, which is to | ||
obey a strict stack discipline. Different kinds of coroutines in SIL have | ||
different representations, reflecting the different capabilities and | ||
structural constraints of different language-level features: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to call async functions a kind of coroutine? They kind of aren't from SIL's perspective; they use mostly exactly the same SIL abstractions as normal functions. Feels like it's just a new dimension of function type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps trying to discuss them as a kind of coroutine only serves to confuse. Thinking of them as just another axis of function type might be easier to understand; I'll try that.
I revised the text a bit; this time I stop trying to discuss async functions as a kind of coroutine, and clarify the semantics of |
It's a natural assumption when reading this introduction that the new
That's totally clear to me, I just don't think it was clear in the docs.
What about an Or, purely hypothetical:
Oh, phew, I completely misread that statement. What I really need to know is that the region of code inside I just reread John's forum post (which you linked above). It's clear that we'll have some magic that blocks the continuation until the begin/await region (operation) complete. I see now that's the whole point of consuming the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some quick comments. I probably have some more.
docs/SIL.rst
Outdated
|
||
sil-instruction ::= 'begin_async_continuation' '[throws]'? sil-type | ||
|
||
%0 = begin_async_continuation $T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jckarter to maintain future flexibility, did you consider making this have potentially multiple instruction results? Not saying it has it today, but if we want to add something this will ensure that we do not run into problems ala apply site.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
begin_async_continuation
doesn't traffic in user-provided values and so doesn't need multiple results. await_async_continuation
does traffic in user-provided values, but I think the usefulness of producing separated values is probably tied to the usefulness of allowing reabstraction of the continuation result — which is to say, per the conversion I had above with Joe, not very useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it isn't useful today, but we should be cognizant that requirements can change in the future. It is easy to treat a multiple result instruction as a single result instruction. Hard to go the other way due to source incompatibility. That being said if we are /really/ sure there is no case in the future where that will be needed. I am cool. Just be aware that we are boxing in the design and potentially (if requirements change) requiring painful refactoring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How hard would it be to add multiple results later if we decide it's useful? For now, YAGNI seems prudent.
docs/SIL.rst
Outdated
|
||
sil-instruction ::= 'begin_async_continuation_addr' '[throws]'? sil-type ',' sil-operand | ||
|
||
%1 = begin_async_continuation_addr $T, %0 : $*T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question about multiple result values here.
docs/SIL.rst
Outdated
await_async_continuation %0 : $UnsafeContinuation<T>, resume bb1 | ||
await_async_continuation %0 : $UnsafeThrowingContinuation<T>, resume bb1, error bb2 | ||
|
||
bb1(%1 : $T): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the ownership of T here? Owned? Can I have something that is guaranteed? It would be good to call this out explicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does always need to be owned, yeah.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I said in the text: " The value of the resume
argument is owned by the current function." Is there a way I can make it clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mark it as @owned like it is in ossa.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bb1(%1 : @owned $T)
I like the new text, thanks! I wonder if I really like |
|
@swift-ci Please smoke test |
Alright, I revised |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this looks great to me, thanks!
Unlike our existing coroutines, async functions run independently within an async coroutine context, and don't
directly yield values back and forth. They therefore mostly behave like normal functions with only an
@async
annotationto indicate the presence of async suspend points. The
withUnsafeContinuation
primitive requires some instructionsto represent the operation that prepares the continuation to be resumed, which will be represented by
begin_async_continuation
...await_async_continuation
regions.