Skip to content
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

Do we need using await const in an async function? #68

Closed
rbuckton opened this issue Sep 21, 2021 · 9 comments
Closed

Do we need using await const in an async function? #68

rbuckton opened this issue Sep 21, 2021 · 9 comments
Labels
question Further information is requested

Comments

@rbuckton
Copy link
Collaborator

The await keyword was added to try using in response to concerns from @erights regarding hidden interleaving points in code execution. While I'm not opposed to keeping it in, I'm curious if @erights or others would have a strong opposition to removing and having @@asyncDispose invoke at the end of the block, if present on a resource:

async function foo() {
  /*a*/
  {
    using const syncResource = getSyncResource(); // has @@dispose
    using const asyncResource = getAsyncResource(); // has @@asyncDispose

    ...
    
    // implicit `await asyncResource[@@asyncDispose]()`
    // implicit `syncResource[@@dispose]()`;
  } 

  ...
}

While its true this would inject an implicit await when the block (a) closes, would this consequence possibly be ameliorated by the fact that using const is novel syntax? using const itself already introduces implicit code evaluation at the end of the block, so its not a stretch to also explain that using const will Await an @@asyncDisposable resource at the end of the block as well.

@erights, if your concern still stands in light of this, I'm happy to leave using await const in. If not, it would be nice to remove the await keyword as unnecessary ceremony.

@rbuckton rbuckton added the question Further information is requested label Sep 21, 2021
@erights
Copy link

erights commented Sep 21, 2021

Hi @rbuckton thanks for asking. It does. In fact, I remain extremely uncomfortable at how weak this is as an interleaving marker to mark an interleaving point which is textually distant and not otherwise marked. However, with the await you already argued me into agreement and I am not withdrawing that agreement. Thanks.

The "novel syntax" doesn't help at all. Ten years from now, it'll just be part of the endless sea of "too much syntax" that all JS programmers suffer with. https://erights.medium.com/the-tragedy-of-the-common-lisp-why-large-languages-explode-4e83096239b9

The current state is that only two keywords mark interleaving points: await and yield. That is about the limit at which these markings can serve their purpose well.

@rbuckton
Copy link
Collaborator Author

I appreciate the feedback, thanks!

@mhofman
Copy link
Member

mhofman commented Sep 21, 2021

I am concerned that dropping try using await blocks causes the interleaving point to become hidden.

In the example above, it's not clear that the block where using appears is special and will cause an interleaving point unless you parse through the block and notice the using await statement in the middle. The same could actually be said for sync resources having code being implicitly called, but that's less of an issue since it's not a stack suspension.

async function foo() {
  /*a*/
  {
    // Potentially some code here    

    using const asyncResource = getAsyncResource(); // has @@asyncDispose

    // More code here
    
    // implicit `await asyncResource[@@asyncDispose]()`
  }

  // ...
}

Compare that to an explicit marker on the containing block (completely arbitrary syntax suggestion):

async function foo() {
  /*a*/
  do {
    // Potentially some code here    

    // Note the use of `async` here instead of `await` as there is no interleaving point
    using async const asyncResource = getAsyncResource(); // has @@asyncDispose

    // More code here
    
    // explicit interleaving point for `await asyncResource[@@asyncDispose]()`
  } await dispose;

  // ...
}

async function blocks would themselves be implicit await dispose blocks as exiting the function implies an interleaving point.

@rbuckton
Copy link
Collaborator Author

rbuckton commented Sep 21, 2021

We've previously discussed the hidden interleaving point in for await (const x of y) {} as being the same. An explicit marker at the bottom is not necessary for for await, and ideally shouldn't be necessary here.

If enforced syntactically it becomes a refactoring hazard. Moving code around within a block would now require additional non-local changes to that code move.

I'd much rather pick a single consistent syntax over having two different ways of doing the same thing (i.e., not having both try using (...) and using const). The move away from try using to using const has broadly been considered a positive direction within TC39, especially from those who encourage RAII patterns. The RAII approach of using const is already under strong consideration to support some of the work that @syg and I have been discussing around cooperative JS and shared code with respect to locking.

Finally, It would be perfectly feasible for a linter to enforce a rule that states a block with a using await const declaration should have a // await dispose comment at the end of the block, much like some linters disallow switch-case fall-through without an explicit comment. Its also feasible for any advanced editor to add decorations/hints to indicate interleaving points without requiring syntax.

@mhofman
Copy link
Member

mhofman commented Sep 21, 2021

We've previously discussed the hidden interleaving point in for await (const x of y) {} as being the same. An explicit marker at the bottom is not necessary for for await, and ideally shouldn't be necessary here.

Right, but at least there is a marker on the block. And since for await is a looping block, an interleaving point when exiting the block is not too much of a stretch since multiple interleaving at the block boundary is already expected. I'd be willing to accept an await marker at the top of the "using" block, but the complete lack of a marker on the block seems like a breaking change with current interleaving semantics.

If enforced syntactically it becomes a refactoring hazard. Moving code around within a block would now require additional non-local changes to that code move.

I'm not sure I follow. Within a block there is no hazard. It would be moving code from a block with marker to a block without. And regardless, if there is syntax missing, the error will be caught by linters and the executing engine.

Finally, It would be perfectly feasible for a linter to enforce a rule that states a block with a using await const declaration should have a // await dispose comment at the end of the block, much like some linters disallow switch-case fall-through without an explicit comment.

But a comment is not code. A switch case without a break will fall through without the statement. The equivalent example here would be that the block without an await marker on it would result in the [@@asyncDispose]() result not being awaited, potentially leading to an unhandled rejection.

In my mind this problem of marking the block is similar to not being able to use await in a non-async function.

@rbuckton
Copy link
Collaborator Author

rbuckton commented Sep 21, 2021

I'm not entirely sure what direction to take with this, then. We've already discussed using const and using await const here: https://github.com/tc39/notes/blob/8711614630f631cb51dfb803caa087bedfc051a3/meetings/2020-02/february-5.md#updates-on-explicit-resource-management, and subsequent discussions on various issue threads have shown that most preferred the using const/using await const forms over the try using and try using await forms. The implicit interleaving point was already discussed in plenary, and we've already discussed and rejected a postfix keyword for this.

The mitigation strategy for handling the implicit interleaving point were:

  • Including await in the using await const declaration.
  • using await appears to the left of const so that it isn't easily hidden elsewhere in the declaration.

This was the consensus as of Feb, 2020, and is the direction I've been taking since (in between other tasks).

@mhofman
Copy link
Member

mhofman commented Sep 21, 2021

I wasn't at the Feb 2020 plenary (or an active TC39 delegate then). If there was consensus on removing any block marking, then that's how it is.

I am in favor of the using const/value style. I'm just concerned with the hidden async interleaving point in unmarked blocks. I understand it would be more syntax, but can we imagine any solution allowing to mark blocks which are not already async (such as async function and for await ... of) ?

@rbuckton
Copy link
Collaborator Author

using await const is only allowed in an already async context (an async function or the top-level of a module). You wouldn't be able to use it (or have an implicit await [@@asyncDispose]()) in code that isn't already parsed in +Await.

@mhofman
Copy link
Member

mhofman commented Sep 21, 2021

Right, but as I mentioned the place of the using await const is not where the interleaving happens (which is why I believe the await word is a bit of a misnomer). The interleaving happens implicitly at the end of the containing block. If that block is a for await ... of or the top level of an async function, then the end of the block is already expected to interleave. All other blocks are unmarked and interleaving would be unexpected. I was just hoping we could find a solution marking those other blocks explicitly, allowing the using await const syntax in it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants