-
Notifications
You must be signed in to change notification settings - Fork 3
Async using syntax #1
Comments
Also I would like to clarify my position from the presentation. If future syntax for async disposables will require an explicit |
Thinking more about this, I believe that the top level block of an |
(please be aware of |
I think having I've never been 100% convinced that such an annotation is strictly necessary. Synchronous That said, I don't think a plain Assuming we were to continue that explicitness guarantee, and could add something like for (using res of iterable) ...; // sync iteration, sync dispose
for (await using res of iterable) ..; // sync iteration, async dispose
for await (using res of asyncIterable) ...; // async iteration, sync dispose
for await (await using res of asyncIterable) ...; // async iteration, async dispose I'm far more likely to drop support for I've previously mentioned that C# (from which the |
I also think that the fact we had to introduce |
I would like to stress as explained above that Both @erights and I want to make sure the place where any interleaving happens is not surprising. In a Given that AsyncDisposableStack.prototype.use first attempts to use asyncDispose, falling back to dispose, i do not find surprising that |
This still doesn't align with how
While I don't disagree that objects shouldn't have both, a platform like NodeJS could reasonably have a |
As @mhofman has stated, this is the wrong place for the |
For what it's worth, I think async function test(){
// synchronous resource and disposal for comparison
using sync = getSyncResource();
// await using
await using db = await getConnection();
// await using implicitly awaits.
await using db = getConnection();
// async using
async using db = await getConnection();
// using async
using async db = await getConnection();
} I think I'd only prefer In regards to explicit block syntax for async disposal, I think that This concept would then combine well with proposals that make new styles of code blocks that have other functionalities: async do expressions and async pattern match enhancement both could be considered as a proper interleave point with an async disposal. This does mean that if you use // async block
await async {
async using db = await getConnection();
}
// async do block
async using toCleanup = await async do {
// This `using` is inside the async do block, so it would get disposed properly
using async pool = await getPool();
// explicit return for clarity, as the syntax hasn't been fully nailed down yet
// Note: no `using` here, since we want this to live until outside the do block.
return await pool.getConnection();
} At the very least, even without an explicit way to specify an async disposal block, any async block should be a candidate for async disposal. And then the disposal can just bubble up to the nearest async block, even if that ends up being the module itself (Top-level await in ESModules). This would also allow async IIFEs to be used for async disposal until there's other more terse ways to create an I feel that the async part of this proposal is more important to have in the language than the synchronous part since most file system support, databases, network, will ideally be async and require async cleanup to handle correctly, and without both parts being added together (or in short succession), I can see people just using the synchronous disposing for these and just swallowing the error ( The semantics of the async disposal would have to be different than sync disposal unless you have to specify that whatever block they are used in is an async block to allow them to work at all, which seems limiting and confusing, OR all blocks in async code will allow for async disposal regardless, which is problematic due to the implicit interleaving that that creates. |
I am leaning toward @erights, do you still have reservations about not having an explicit If so, I will continue with my plan to postpone |
I believe As I mentioned, I'm still not sure the explicit |
I suspect that very, very few people are aware that the end of a for-await-of loop is an interleaving point, and I don't think that's actually caused many problems in practice. So I personally don't think that restriction makes sense. |
As @mhofman says, the fact that a function is annotated with
Given that we allow a top level |
We have been working hard on formalizing some safety rules around interleaving points. In the process, I've been amazed at how dangerous undisciplined use of |
I'm mostly just sounding out the concerns. Top-level I'd be tempted to move forward with a restriction such that Since the concern about an explicit marker for the interleave point stands, I see only a few options that would continue along the path of RAII-style
Based on my understanding of how async function f() {
// top level
async using x = ...; // ok
// nested
await async do {
async using y = ...; // ok
}; // dispose y
// back at top level
} // dispose x Alternatively, an explicit async function f() {
// top level
async using x = ...; // ok
// nested
async {
async using y = ...; // ok
} // dispose y
// back at top level
} // dispose x |
I like this analysis. I think we're converging. Thanks! |
If I had to choose, I'd pick the The downside of |
In my opinion The unconditional Await in |
It indicates an async interleave point as much as |
It is not the same. The body of the function does not have an interleaving point, it's the promise returned by the function which "awaits" the disposal. |
Yet we would allow My main point is that an |
Hi @rbuckton , respectfully, you're missing @mhofman 's point. There is no interleaving point at the end of an async function body because there is no control-flow within the function after the end of the function body. The current invariant: "All interleaving points are marked with a The closest we have to a violation is the one that you pointed out: The implicit |
Apologies, I was trying to clarify why we would allow Per your position, async function f() {
...
async do {
async using x = ...;
// rest of block completes synchronously
}
// uh oh, `x` may not actually be disposed yet since we forgot to 'await'
} Also, since await async do {
}
(foo); // potentially interpreted as `await (async do {}(foo))`, depending on TBD spec In lieu of async function f() {
// explicit 'await' demarcation
// indicates exactly what we're going to 'await'
// 'using {}' not legal on its own, so can't forget 'await'
// 'await {}' is legal, but is not a Block, so can't forget 'using'
await using {
async using x = ...;
// rest of block completes synchronously (or asynchronously)
}
// ok, 'x' should be disposed
} And an Unfortunately, if/when |
A much more serious problem is that async function f(){
await async do {
return;
};
} is not legal. It can't be, because without the Since |
There was some discussion about this on Matrix. |
Wouldn't a currently existing |
@erights, perhaps you can clarify your concerns regarding I personally favor just using a plain Block, as I still believe the async async function foo() {
using await x = ...;
{
someCodeBeforeUsing();
using await y = ...;
someCodeAfterUsing();
}
} This matched languages like C#, which also has an async The switch to an In addition, IDEs like VS Code and Eclipse can perform syntax highlighting and add text editor decorations. These could easily be used to flag such blocks for you, much like VS Code's inlay hints for parameter names, meaning even lint rules aren't strictly necessary. |
Pinging @kriskowal since they expressed interest in this topic on Matrix during the plenary. |
Also, one quick clarification: I don't want us to spend too much time dwelling on the actual spelling of the async I've generally been spelling the async
I may use them interchangeably as part of this discussion, but I'm not tied to either spelling. However, these spellings were chosen for specific reasons:
|
I'm answering on behalf of @erights. We've discussed this again since the last plenary and after the Matrix discussion that stemmed from questions raised by @syg and @bakkot. We still strongly believe that the programmer must be able to reason about where asynchronous interleaving point happen. The fact that most programmers don't pay attention doesn't mean it's not important, it just means they haven't realized how interleaving may impact their program's execution. That said, we are willing to abandon our requirement that the exact point of interleaving be marked by an explicit One question that came up however is regarding the conditionality of the interleaving point when exiting a block. Our above requirement implies that the programmer should be able to infer the presence of an interleaving point based on the syntactic content of the block. We believe the best way to accomplish this while not changing existing execution semantics is to mandate an interleaving point when exiting a block if the block contains an |
In other words, any How does that interact with eval (direct or indirect)? Do |
I would say that
|
To clarify, would this syntax meet that requirement? {
using await x = getSomeAsyncResource();
} Given that:
This means that, given consistent indentation and formatting, you can easily scan down a column within a block to observe any
I'm not sure I understand why you would force an async interleaving point in that case. If you are writing code that expects a given block will have "exactly N interleaving points", then you are running afoul of "releasing Zalgo". If you are writing code that expects a given code block to be asynchronous, but it happens to complete synchronously, then you would already be protected, so introducing an extra interleaving point is unnecessary. This would be like requiring a block to introduce an interleaving point in an x: {
if (y) break x;
await p;
} |
I don't object to this requirement, but I also don't understand it - programmers understand that an
My assumption would be that the eval creates an implicit "block" which would contain the I think the current spec actually doesn't handle |
My mental model is that when an
Yes. And I was sure to not debate the
I'm not sure I follow. I don't see how forcing an interleaving point would release Zalgo if that interleaving point is unconditional. Sometimes asynchronous blocks is what we're concerned about, especially in the case of exceptions. It would also be harder to explain what happens when an |
Writing code that depends on counting interleaving points has the potential to release Zalgo. That is the case whether this always has an implicit interleaving point or not. A user might try to depend on the existence of a forced interleaving point to try to run code in between microtasks. The potential to "release Zalgo" in either case is purely based on the code that the user writes. Since counting interleaving points in either case has the potential to "release Zalgo", I generally favor the approach that doesn't introduce unnecessary extra delays.
"Sometimes asynchronous blocks" already exist (i.e., my
I do not share the same mental model, even if there is overlap. If the spec were to use a stack in this way, I imagine I'd conditionally initialize it at the time the first |
|
Although this is a weaker preference, I also don't think we should have a forced interleave point if the {
using await x = null;
} // nothing will be disposed, why force a delay? And the way the spec is written currently, there isn't even an interleave point if the |
That is definitely a non-starter for us on the same grounds that
The Zalgo issues we're concerned about have to do with the maybe existence of an interleaving point, not how many interleaving points may exist. We will try to write something to better explain our concerns, but it's basically a regular Zalgo issue of a continuation that is sometimes executed synchronously and sometimes not. |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
We did in fact argue about it in tc39. The mandatory tick won on Zalgo-prevention grounds. |
Right, the Zalgo problems are in general not about the number of ticks if 1 or more, but whether there are 0 or 1 ticks. Of course some code out there will be sensitive about the number of ticks, but I really don't care about those. |
The Zalgo issue is a complex topic, but the main intent of the original article was to address how asynchronous APIs should be written, and focused primarily on how to handle asynchronous completion. You don't want an API whose result is sometimes available synchronously, and sometimes available asynchronously. That generally means that you have to wait a tick to observe a result, which is why all User code isn't an API, it produces an API. An async function like the following is reasonable, and whether or not it uses async function foo(x) {
z: {
if (x) break z;
await bar();
}
}
await foo(); I think it's perfectly reasonable to have the same expectations in this code as well: async function foo(x) {
z: {
if (x) break z;
using await y = bar(); // this code is never hit
}
}
await foo();
That is acceptable to me. As I said above, the conditionality of |
I put up a PR (#6) for what we've been discussing here:
for await (using await x of y) ; However, I think this is consistent given the following matrix: // sync iteration, sync disposal
for (using x of y) ;
// sync iteration, async disposal
for (using await x of y) ;
// async iteration, sync disposal
for await (using x of y) ;
// async iteration, async disposal
for await (using await x of y) ; While having two for (const x of y) ; // @@iterator
for await (const x of y) ; // @@asyncIterator, @@iterator
using x = y; // @@dispose
using await x = y; // @@asyncDispose, @@dispose |
I've seen different syntax suggestions related to async disposal spread over different issues, in particular tc39/proposal-explicit-resource-management#16 tc39/proposal-explicit-resource-management#76 tc39/proposal-explicit-resource-management#84 #4. I wanted to concentrate the discussion in a single issue.
I'd first like to discuss the ongoing assumption that
using
an async-disposable requires specific syntax.The way I model the
using
syntax is that a block containing ausing
declaration implicitly creates aDisposableStack
. Everyusing
declaration implicitly results in a.use()
on the stack, and that exiting the block implicitly calls[Symbol.dispose]()
on the stack.Now if we assume that we have a concept of "async blocks", instead of a
DisposableStack
being implicitly created, it'd be anAsyncDisposableStack
. At that point anyusing
declaration would similarly call the.use()
of the implicit async disposable stack, which is a synchronous operation, and it's the exit of the "async block" which awaits the[Symbol.asyncDispose]()
call.As such I would be against any syntax that uses the
await
keyword for theusing
declaration, as nothing is awaited at the declaration time. Similarly I would be against any syntax that doesn't use theawait
keyword on the block as a marker of interleaving. I would be ok with aasync using
declaration to be explicit, but I would consider that unnecessary verbosity.Given the mental model above, I believe it would be entirely natural that
for await (using r of ...)
, andusing
declarations insidefor-await-of
blocks in general, would implicitly use anAsyncDisposableStack
.for-await-of
is conceptually the only "async block" in the language at the moment. Then it become a matter of specifying what the syntax of an "async block" not tied to iteration looks like.The text was updated successfully, but these errors were encountered: