Closed dckc closed 2 years ago
It is not primarily the hazard keeping it out of Jessie, though that is an issue.
The hazard is that, when both writing and reading a program, especially when reading other people's programs, an explicit await
is not salient enough for people to understand that there's a turn boundary there, and that arbitrary turns interleave at that point, invalidating stateful assumptions. I have looked through many tutorials on when to use await
in async programming, and NOT ONE explains the need to think about this when writing or seeing an await
in code.
One of the goals of Jessie is that it be easily implementable by a straightforward eval/apply style interpreter on top of a great variety of other languages. Both yield
and await
either require
The first two are not simple. The last is not universally available across desired host languages.
Interesting discovery since then, due to @dtribble: async
functions without await
The semantics of an async function without await
is that its outcome is always turned into a returned promise. If the function body returns a non-thenable, the function returns a promise fulfilled to that non-thenable. If the function body throws something, the function returns a promise rejected with that something as the reason. This is useful.
Thus, we will include async
functions in Jessie.
By the same reasoning, it would be safe to include generators (function *
) without yield
in Jessie. But is there any reason to?
And finally, it would be safe to include async generators (async function*
) without yield
or await
in Jessie. But again, is there any reason to?
FWIW, my use case is using JavaScript tools to develop contracts to run on RChain, with hopes of actually running Jessie / Zoe / ERTP contracts on RChain.
For example, 2.check_balance.rho begins with the following, where RevVaultCh
, vaultCh
, and balanceCh
are a boring syntactic burden, due to get a shorthand syntax in rholang 1.1:
new
rl(`rho:registry:lookup`), RevVaultCh,
vaultCh, balanceCh,
stdout(`rho:io:stdout`)
in {
rl!(`rho:rchain:revVault`, *RevVaultCh) |
for (@(_, RevVault) <- RevVaultCh) {
...
I have convinced the machine to take check_balance.js starting this way:
import { tuple } from '@rchain-community/js2rho';
import rl from 'rho:registry:lookup';
import E from '@agoric/eventual-send';
export default
async function main() {
const { _0: _, _1: RevVault } = await E(rl)('rho:rchain:revVault');
...
and deal with the boring syntax bits for me to produce check_balance.rho:
new rl(`rho:registry:lookup`),
console(`rho:io:stdout`)
in {
new AwaitExpression_9c36_0
in {
rl!("rho:rchain:revVault", *AwaitExpression_9c36_0)
|
for(@{ (*_, *RevVault) } <- AwaitExpression_9c36_0) {
...
The AwaitExpression
tag in the estree made this particularly straightforward, but perhaps I could recognize E(...).then(...)
without too much more difficulty.
await
is just sugar for the code CPS transformed into using explicit promises and .then
. So yes, I recommend recognizing explicit promise patterns, including E
and tildot ~.
Above, you almost certainly did not mean E(...).then(...)
. This would try to send then(...)
as a remote message to whatever the promise within the E(...)
designates. For the equivalent of await
you should recognize
Promise.resolve(...).then(...)
or its HandledPromise
equivalent.
@michaelfig and I are thinking about adding
E.when(..., ...)
as a brief convenience for
HandledPromise.resolve(...).then(...)
We have now decided to add async
functions, and a restricted form of await
within them. Using pseudo-bnf, the idea is something like:
functionBody ::= (topLevelDeclaration | topLevelStatement)*;
topLevelDeclaration ::=
("const" | "let") destructingPattern "=" "await" expression ";"
| declaration; // existing Jessie declaration without "await"
topLevelStatement ::=
"await" expression ";"
| statement; // existing Jessie statement without "await"
Your example above
async function main() {
const { _0: _, _1: RevVault } = await E(rl)('rho:rchain:revVault');
...
}
is within this grammar, because it occurs only at the top level of a function, and it precedes the entire initialization expression.
This should adequately satisfy both criteria:
then
s. Separately, we may have an a specialized lint rule suggesting that an // AWAIT
comment occur immediately after these topLevelDeclarations and topLevelStatements..then
form.Further, a bit of experience shows that it covers most of the practical pain we seem to encounter from avoiding await
The permitted form would exclude catching an exception, right?:
async function main() {
try {
await E(x).foo();
} catch (err) {
react_to(err);
}
}
I think that's probably fine, and I imagine the interpreter/transform would be more complicated if it needed to allow this case.. just wanted to check.
The permitted form would exclude catching an exception, right?:
Yes, our discussion thus far has excluded that form. It's easy enough to break such a try
into a separate (async) function, then use myFn().catch(...)
.
What would be an example of a use of await
that wouldn't be allowed?
@warner 's example above is one. Some others:
// Can't nest `await` within an expression
foo(await bar());
// can't use within a control construct
if (await foo()) { ... }
if (...) { await foo(); }
Surprising
// can't use on return expression
return await foo();
With regard to our criteria, we could allow it. However, the await
here has no observable effect, but it is not obvious that it has no effect, so it is good that our simple rules happen to ban it.
// can't use on return expression return await foo();
With regard to our criteria, we could allow it. However, the
await
here has no observable effect, but it is not obvious that it has no effect, so it is good that our simple rules happen to ban it.
Eslint rules also ban it, at least the ones we use.
parse-time detection of async / await is too handy for a js2rho project I'm working on, but I recall some discussion of the hazard.
Would you please either