Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Add async and a restricted form of await #45

Closed
dckc opened this issue Apr 17, 2020 · 12 comments
Closed

Add async and a restricted form of await #45

dckc opened this issue Apr 17, 2020 · 12 comments
Assignees

Comments

@dckc
Copy link
Contributor

dckc commented Apr 17, 2020

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

  • add async / await to Jessie, or
  • write up the hazzard?
@erights
Copy link
Contributor

erights commented Apr 17, 2020

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

  • a CPS (continuation passing style) transformation,
  • a much more complex interpreter,
  • or a host language with similar power to suspend and resume an activation.

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

  • have no interleaving hazard
  • do not make Jessie harder to implement or less universal.

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?

@dckc
Copy link
Contributor Author

dckc commented Apr 17, 2020

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.

@erights
Copy link
Contributor

erights commented Apr 18, 2020

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.

@erights
Copy link
Contributor

erights commented Apr 18, 2020

@michaelfig and I are thinking about adding

E.when(..., ...)

as a brief convenience for

HandledPromise.resolve(...).then(...)

@erights erights self-assigned this Jul 11, 2020
@erights
Copy link
Contributor

erights commented Jul 11, 2020

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:

  1. It is not too hazard prone to reason about interleaving. With practice, it may well be less hazardous than reasoning about thens. Separately, we may have an a specialized lint rule suggesting that an // AWAIT comment occur immediately after these topLevelDeclarations and topLevelStatements.
  2. It is not onerous to implement even in a very simple eval/apply interpreter written in other language. It is even simple enough that it could be implemented directly in the interpreter rather than by a cps rewrite. If one did do a rewrite, it would be a trivial form of cps that would not lose readability compared to either the original or the explicit .then form.

@erights
Copy link
Contributor

erights commented Jul 11, 2020

Further, a bit of experience shows that it covers most of the practical pain we seem to encounter from avoiding await

@erights erights changed the title async / await: add or document pitfall Add async and a restricted form of await Jul 13, 2020
@warner
Copy link

warner commented Jul 13, 2020

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.

@michaelfig
Copy link
Member

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(...).

@katelynsills
Copy link
Contributor

What would be an example of a use of await that wouldn't be allowed?

@erights
Copy link
Contributor

erights commented Jul 13, 2020

@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(); }

@erights
Copy link
Contributor

erights commented Jul 13, 2020

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.

@michaelfig
Copy link
Member

// 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.

@endojs endojs locked and limited conversation to collaborators May 29, 2022
@michaelfig michaelfig converted this issue into discussion #80 May 29, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants