A final proposal for await syntax
This is an announcement regarding the resolution of the syntax for the await operator in Rust. This is one of the last major unresolved questions blocking the stabilization of the async/await feature, a feature which will enable many more people to write non-blocking network services in Rust. This post contains information about the timeline for the final decision, a proposal from the language team which is the most likely syntax to be adopted, and the justification for this decision.
In brief: we intend to make a final decision on May 23, and we currently favor adopting the “dot await” postfix syntax. All of this is elaborated further in this document.
Timeline
We hope to achieve a timeline of stabilizing what we call the “minimum viable product” (MVP) of async/await in the 1.37 release of Rust. This release will happen on August 15th, and the beta for the release will be cut on July 4th. This means we have until roughly the end of June (2 months) to land a stabilization PR in order to hit this target.
There’s a fair amount of work needed to stabilize even the MVP of async/await. There are still implementation issues that we consider blocking on the MVP, and we need to develop clear documentation - not only about how to use the feature, but about setting expecations regarding the current status of the feature and its long term development. More on this is forthcoming.
However, the big outstanding issue blocking stabilization which is the responsibility of the language design team to resolve is the syntax of the await operator. This post contains a proposal which the language team found consensus on in the last meeting, as well as its justification and its most viable alternative.
We will make a final decision in the May 23 meeting of the language design team. Until then we will do our best to actively engage with the community discussion of this proposal and its justification, and are open to hearing counterproposals.
I want to add, however, that the amount of feedback regarding syntax for the await operator has been overwhelming. It has also devolved into a situation in which many commenters propose to reverse previous decisions about the design of async/await on which we have already established firm consensus, or otherwise introduced possibilities we have considered and ruled out of scope for now. This has been a major learning experience for us: one of the major goals of the “meta” working group will be to improve the way groups working on long-term design projects communicate the status of the design with everyone else.
In the meantime, this document hopes to explain well the decision that we’ve reached on the specific question of the await operator’s syntax and why. I firmly hope that everyone will extend to us good faith that we have considered this decision at length and very seriously, and have discussed and thought about all options.
The Lang Team’s Proposal
The lang team proposes to add the await operator to Rust using this syntax:
expression.await
This is what’s called the “dot await” syntax: a postfix operator formed by the combination of a period and the await keyword. We will not include any other syntax for the await operator.
Justification & Notes
A postfix keyword operator
Our previous summary of the discussion focused most of our attention on the prefix vs postfix question. In resolving this question, there was a strong majority in the language team that preferred postfix syntax. To be concrete: I am the only member of the language team that prefers a prefix syntax. The primary argument in favor of postfix was its better composability with methods and the ? operator. It was also clear that the discussion had reached a standstill; the arguments had all been made. For this reason, it seemed obvious to all of us (including me) that adopting a postfix syntax was the best way to ship the feature.
Having chosen a postfix syntax, this becomes the problem: because we also have consensus that the operator should include the string “await,” this becomes the first postfix keyword operator in Rust. We need to decide what the syntax for this sort of production will be - not only the await operator, but also any future similar operators. We believe the best choice for this kind of construct is a period character joined with a non-contextual keyword. Hence, we propose to introduce the await operator using the “dot await” syntax.
About method-like and macro-like syntax
More popular in the community discussions than dot await has been two other alternatives: one in which the await operator mimics a method call, and one in which the await operator mimics a macro. These two syntaxes share a weakness which led us to prefer dot await: the await operator is not and cannot be either a method or a macro.
Await is not a method because it yields control in the same way that ?
is not
a method. Though users have repeatedly asked why there is not an “Await trait”
that defines the await method, the reality is that there is no way to define
the await operator as a method. The user defineable part of the await
operator is controlled by the Future trait, which defines what happens when
the await operator polls the future. This is analogous to the way that the user
defineable part of the ?
operator is controlled by the Try trait, which does
not impact the control flow aspect of the operator.
The situation with macro-like syntax is a little more nuanced for a few
reasons. First, macros can influence control flow. A lot has been made of the
similarity to the evolution from try!
to ?
. However, macros can only
influence control flow by expanding to code that the user could have written
themselves - the try macro was defined in the standard library and expanded to
code using the return
operator. In the long term, await cannot be defined as
a macro in this way because the form of yielding it represents cannot be
expressed by the user in the surface syntax. Therefore, await is not a macro.
This is confused by the odd (and frankly hacky) history and status quo of the
await operator. Async/await was original implemented out of tree in the
futures-async-await crate. This crate defines
async/await as a macro on top of the unstable generators feature, expanding to
a function that returns a generator and uses the yield
operator internally.
And now that async/await is built into the compiler, await still is a macro,
expanding to yield. This is incorrect and is one of the main implementation
blockers before we can stabilize async/await. It leads to bad diagnostics and
“leaks”, allowing code to compile which certainly must not be allowed, such as:
#![feature(async_await, generators)]
// This function compiles with an unreachable code warning, but yield is not
// supposed to be allowed outside of generators.
pub async fn bad() {
yield panic!();
}
(As a side note, the decision to have async/await as first class syntax instead of continuing the crate which built it on top of generators was made with good reason and after seriously considering the design space. First class syntax makes it much easier to provide first class diagnostics, and making async/await and generators separate, orthogonal features makes it much easier to solve the problem of defining Streams. This decision was made more than a year ago and is not open for reconsideration at this phase of the design.)
So, the weirdness of our current situation notwithstanding, the await operator will not be implemented as a macro. There is yet another wrinkle. Rust defines several so-called “compiler built-ins” which are accessed using syntax that looks like macros even though they are not macros. It is reasonable to argue, then, that the await operator could be treated as a compiler built-in. However, await has very little in common with these compiler built-ins and it does not seem appropriate to treat it as one of these. Compiler built-ins tend to fall into two categories:
- Most compiler built-ins are isolated features that are sort of like “breaking
the fourth wall” and give access to information about the program or
compilation itself: for example, the
line!()
built-in gives you the current line number in the source code file. - A small number of built-ins introduce domain-specific languages that would be
invalid Rust outside of that built-in. This refers to
format_args!
andmacro_rules!
. These are implemented this way for historical reasons: in theory,format_args
could be implemented today as a procedural macro provided by std, andmacro_rules!
is a legacy system we intend to deprecate and replace with themacro
keyword.
Await is neither of these. It is a fundamental control flow operator that must be paired with the async keyword and interacts with the rest of the function as a whole. It would not make sense to decide the await operator functions as a compiler built-in.
Some have suggested that the !
should be interpreted as indicating any sort
of control flow, not only macros. But this seems suspect: the vast majority of
macros do not perform any control flow, and none of the built in control flow
constructs use the !
character.
“.keyword” and field accesses
The counterargument can be made, of course, that the dot keyword syntax conflicts with field access. The await operator is also not a field access. There are a few things which separate the dot keyword syntax from the “method” or “postfix macro” syntax which make us more comfortable with this.
First, its hard to ignore that dot await is a strict subset of the characters used in all of these products. Its very easy to build a mental model of the period operator as simply the introduction of all of these various postfix syntaxes: field accesses, methods, and certain keyword operators like the await operation. (In this model, even the ? operator can be thought of as a modification on the period construct.)
There is also the fact that a user who knows anything about the semantics of the await operator will quickly realize that its not possible that await is actually a field access. In contrast, you need to know a lot about the semantics of Rust to understand that await cannot be implemented as either a method or a macro. That is to say, a user who is initially confused about the relationship between this construct and field accesses will come more quickly to understand that this is a built-in construct different from field access than they would be if we used methods or macros.
We also believe that there is a good amount of mitigation of the problem of having an initial confusion about whether this operator is a field access. First, by using a reserved keyword, it will be differentiated by any syntax highlighting system. Users are very likely to understand that this is different from a field access on first witnessing the syntax if it is highlighted. Even if not using highlighting, many users will be familiar with languages using an async/await syntax; while those languages will probably have a different syntax for the await operator, they will still recognize the await keyword as indicating something unusual is happening.
Space await: the most viable alternative
However, we do recognize the confusion with the existing field access construct as the most serious downside of the dot await syntax. For that reason, we did consider using a syntax which does not have any potential for confusion with an existing construct.
We consider the most viable of these syntaxes the space await syntax. Other
choices (like “expression@await
” or “expression#await
”) suffer from too
much from the “line noise” problem and we strongly prefer to avoid introducing
new meanings to punctuation characters for this purpose. For that reason, we
discussed expression await
as the primarily alternative to
expression.await
.
However, we still did not find strong support for this alternative over the dot await notation. It has some serious downsides of its own, which we found to be more bothersome than the possibility of confusion that dot await introduces:
- It does not visually group well, making it seem like await and anything attached to it are a separate expression from the preceding subexpression.
- Postfix space expressions can cause problems with compiler recovery when the user omits a necessary semicolon (that is, they can result in the compiler giving much less helpful error messages); we’d like to avoid them without a compelling motivation.
In particular, some members of the language team are excited about a potential
future extensions in which some “expression-oriented” keywords (that is, those
that evaluate to something other than !
or ()
) can all be called in a
“method-like” fashion. In this world, the dot await operation would be
generalized so that await were a “normal” prefix keyword, but the dot
combination applied to several such keywords, most importantly match:
foo.bar(..).baz(..).match {
Variant1 => { ... }
Variant2(quux) => { ... }
}
This does not work very well with the space-based syntax, particularly for the compiler recovery reasons.
Concluding notes
I hope in reading this that users recognize that we found every potential syntax for the await operator to have non-trivial downsides. Ultimately, the decision was a weighing of the downsides and their mitiations, and trying to find the least bad option. I hope everyone can understand that we do not claim the dot await operator is a completely perfect solution, only that we reached consensus that it was, in our appraisal, the least imperfect.
We’ll continue to solicit and consider feedback until the May 23 meeting. It’s possible that new arguments will be presented in this time that will seriously change the calculus and cause a different outcome, but users should be realistic about the amount of consideration this question has already received and how likely it is that truly novel information will be introduced at this phase. This specific question - the await operator syntax - has been the most discussed decision the lang team has ever made, with more than 1000 comments in total already. Thanks again to everyone who participated in these discussions!
Even if you might have prefered a different outcome on this question, I hope you will remember the bigger picture. Shipping an async/await syntax in Rust will enable a huge number of users and potential users to write highly efficient network services using Rust, a memory safe language. The impact of shipping this feature for our project is enormous, and also (if I might be a little immodest about Rust for a moment) significant for the software industry as a whole. This is the important thing to focus our attention on.