-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Support using and await using declarations
#54505
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
Conversation
f500987 to
587a1a4
Compare
| } | ||
|
|
||
| // `typeNode` is not merged as it only applies to comment emit for a variable declaration. | ||
| // TODO: `typeNode` should overwrite the destination |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left this TODO since changing this is out of scope for this PR.
weswigham
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs declaration emit tests, since these can be pulled into declaration emit via typeof types nodes, eg
await using d1 = { async [Symbol.asyncDispose]() {} };
export type ExprType = typeof d1;I'm pretty sure we'll need to transform them to normal non-using variable declarations, since there's no disposal stuff as far as the types care.
| dispose = value[Symbol.dispose]; | ||
| } | ||
| if (typeof dispose !== "function") throw new TypeError("Object not disposable."); | ||
| env.stack.push({ value: value, dispose: dispose, async: async }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated: at what point will it be OK for us to use es6 features like object shorthands in our esnext downlevel helpers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If our helpers were an AST instead of a string, then we could arguably downlevel them on demand. Unfortunately, that wouldn't work for tslib. Since we aren't downleveling the helpers, I'd say we can use new syntax only if we retire --target es5 and --target es3.
sandersn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial comments before looking at the tests.
| } | ||
| declare var SuppressedError: SuppressedErrorConstructor; | ||
|
|
||
| interface DisposableStack { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To check my understanding, is this a user-visible utility class to allow people to non-disposables to be disposed, and avoid disposal at the end of the block if they choose?
Is use basically equivalent to using, except that it also makes the disposable managed by the stack?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, use is the imperative equivalent of using. One way to conceptualize this is that
{
using x = getX();
using y = getY();
doSomething();
}is roughly equivalent to
const stack = new DisposableStack();
try {
const x = stack.use(getX());
const y = stack.use(getY());
doSomething();
}
finally {
stack[Symbol.dispose]();
}(except that using has additional semantics around handling error suppressions caused by disposal)
|
One of the RAII patterns I've used in C++ was to acquire a lock within a scope by constructing a variable. That variable wouldn't get referenced at all beyond its declaration because it's just used for automatic cleanup. One thing I found kind of "off" was that in the current implementation:
I think it probably makes sense to make the same exception we have for parameters, exempting them from this check as long as they're prefixed with an underscore. If we want, we can limit this to just |
sandersn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of questions from me.
I think that makes sense, and I can look into that today. The proposal used to include a bindingless form a la |
|
One thing that I think we should seriousy consider is whether The thing I'm wary of is something like |
I'm not sure I like using the |
|
I've modified |
If there were a global const stack = new DisposableStack();
stack.defer(() => { ... });I think we're more likely to see special-case disposables that use a more specific name, such as a built-in class SafeHandle<T> {
static #dispose = ({ unsafeHandle, cleanup }: { unsafeHandle: T, cleanup: (unsafeHandle: T) => void }) => {
cleanup(unsafeHandle);
};
static #registry = new FinalizationRegistry(SafeHandle.#dispose);
#unregisterToken = {};
#data: { unsafeHandle: T, cleanup: (unsafeHandle: T) => void } | undefined;
constructor(unsafeHandle: T, cleanup: (unsafeHandle: T) => void) {
this.#data = { unsafeHandle, cleanup };
SafeHandle.#registry.register(this, this.#data, this.#unregisterToken);
}
get unsafeHandle() {
if (!this.#data) throw new ReferenceError("Object is disposed");
return this.#data.unsafeHandle;
}
dispose() {
if (this.#data) {
SafeHandle.#registry.unregister(this.#unregisterToken);
const data = this.#data;
this.#data = undefined;
SafeHandle.#dispose(data);
}
}
[Symbol.dispose]() {
return this.dispose();
}
} |
|
@weswigham: I addressed the declaration emit request. Do you have any further feedback? |
This adds support for the
usingandawait usingdeclarations from the TC39 Explicit Resource Management proposal, which is currently at Stage 3.NOTE: This implementation is based on the combined specification text from tc39/proposal-explicit-resource-management#154, as per TC39 plenary consensus to merge the sync and async proposals together now that they both are at Stage 3.
Overview
A
usingdeclaration is a new block-scoped variable form that allows for the declaration of a disposable resource. When the variable is initialized with a value, that value's[Symbol.dispose]()method is recorded and is then invoked when evaluation exits the containing block scope:An
await usingdeclaration is similar to ausingdeclaration, but instead declares an asynchronously disposable resource. In this case, the value must have a[Symbol.asyncDispose]()method that will beawaited at the end of the block:Disposable Resources
A disposable resource must conform to the
Disposableinterface:While an asynchronously disposable resource must conform to either the
Disposableinterface, or theAsyncDisposableinterface:usingDeclarationsA
usingdeclaration is a block-scoped declaration with an immutable binding, much likeconst.As with
const, ausingdeclaration must have an initializer and multipleusingdeclarations can be declared in a single statement:No Binding Patterns
However, unlike
const, ausingdeclaration may not be a binding pattern:Instead, it is better to perform destructuring in a secondary step:
NOTE: If option (b) seems less than ideal, that's because it may indicate a bad practice on the part of the resource producer (i.e.,
getResource()), not the consumer, since there's no guarantee thatxandyhave no dependencies with respect to disposal order.Allowed Values
When a
usingdeclaration is initialized, the runtime captures the value of the initializer (e.g., the resource) as well as its[Symbol.dispose]()method for later invocation. If the resource does not have a[Symbol.dispose]()method, and is neithernullnorundefined, an error is thrown:As each
usingdeclaration is initialized, the resource's disposal operation is recorded in a stack, such that resources will be disposed in the reverse of the order in which they were declared:Where can a
usingbe declared?A
usingdeclaration is legal anywhere aconstdeclaration is legal, with the exception of the top level of a non-module Script when not otherwise enclosed in a Block:This is because a
constdeclaration in a script is essentially global, and therefore has no scoped lifetime in which its disposal could meaningfully execute.Exception Handling
Resources are guaranteed to be disposed even if subsequent code in the block throws, as well as if exceptions are thrown during the disposal of other resources. This can result in a case where disposal could throw an exception that would otherwise suppress another exception being thrown:
To avoid losing the information associated with the suppressed error, the proposal introduced a new native
SuppressedErrorexception. In the case of the above example,ewould beallowing you to walk the entire stack of error suppressesions.
await usingDeclarationsAn
await usingdeclaration is similar tousing, except that it operates on asynchronously disposable resources. These are resources whose disposal may depend on an asynchronous operation, and thus should beawaited when the resource is disposed:Allowed Values
The resource supplied to an
await usingdeclaration must either have a[Symbol.asyncDispose]()method or a[Symbol.dispose]()method, or be eithernullorundefined, otherwise an error is thrown:Please note that while a
[Symbol.asyncDispose]()method doesn't necessarily need to return aPromisein JavaScript, for TypeScript code we've opted to make it an error if the return type is notPromise-like to better surface potential typos or missingreturnstatements.Where can an
await usingbe declared?Since this functionality depends on the ability to use
await, anawait usingdeclaration may only appear in places where anawaitorfor await ofstatement might be legal.Implicit
awaitat end of BlockIt is important to note that any Block containing an
await usingstatement will have an implicitawaitthat occurs at the end of that block, as long as theawait usingstatement is actually evaluated:This can have implications on code that follows the block, as it is not guaranteed to run in the same microtask as the last statement of the block.
forStatementsusingandawait usingdeclarations are allowed in the head of aforstatement:In this case, the resource (
res) is not disposed until either iteration completes (i.e.,res.doneistrue) or theforis exited early due toreturn,throw,break, or a non-localcontinue.for-inStatementsusingandawait usingdeclarations are not allowed in the head of afor-instatement:for-ofStatementsusingandawait usingdeclarations are allowed in the head of afor-ofstatement:In a
for-ofstatement, block-scoped bindings are initialized once per each iteration, and thus are disposed at the end of each iteration.for-await-ofStatementsMuch like
for-of,usingandawait usingmay be used in the head of afor-await-ofstatement:It is important to note that there is a distinction between the above two statements. A
for-await-ofdoes not implicitly support asynchronously disposed resources when combined with a synchronoususing, thus anAsyncIterable<AsyncDisposable>will require bothawaitsinfor await (await using ....switchStatementsA
usingorawait usingdeclaration may appear in in the statement list of acaseordefaultclause aswitchstatement. In this case, any resources that are tracked for disposal will be disposed when exiting the CaseBlock:Downlevel Emit
The
usingandawait usingstatements are supported down-level as long as the following globals are available at runtime:Symbol.dispose— To support theDisposableprotocol.Symbol.asyncDispose— To support theAsyncDisposableprotocol.SuppressedError— To support the error handling semantics ofusingandawait using.Promise— To supportawait using.A
usingdeclaration is transformed into atry-catch-finallyblock as follows:The
env_variable holds the stack of resources added by eachusingstatement, as well as any potential error thrown by any subsequent statements.The emit for an
await usingdiffers only slightly:For
await using, we conditionallyawaitthe result of the__disposeResourcescall. The return value will always be aPromiseif at least oneawait usingdeclaration was evaluated, even if its initializer wasnullorundefined(see Implicitawaitat end of Block, above).Important Considerations
super()The introduction of a
try-catch-finallywrapper breaks certain expectations around the use ofsuper()that we've had in a number of our existing transforms. We had a number of places where we expectedsuper()to only be in the top-level statements of a constructor, and that broke when I started transforminginto
The approach I've taken in this PR isn't perfect as it is directly tied into the
try-catch-finallywrapper produced byusing. I have a longer-term solution I'm considering that will give us far more flexibility withsuper(), but it requires significant rework ofsuper()handling in thees2015transform and should not hold up this feature.Modules and top-level
usingThis also required similar changes to support top-level
usingin a module. Luckily, most of those changes were able to be isolated into the using declarations transform.Transformer Order
For a number of years now we have performed our legacy
--experimentalDecoratorstransform (transforms/legacyDecorators.ts) immediately after thetstransform (transforms/ts.ts), and before the JSX (transforms/jsx.ts) and ESNext (transforms/esnext.ts) transforms. However, this had to change now that decorators are being standardized. As a result, the native decorators transform (transforms/esDecorators.ts) has been moved to run after the ESNext transform. This unfortunately required a bit of churn in both the native decorators transform and class fields transform (transforms/classFields.ts). The legacy decorators transform will still run immediately after thetstransform, since it is still essentially TypeScript-specific syntax.Future Work
There is still some open work to finish once this PR has merged, including:
tslibfor the new helpers.try-finallycontaining a resource into ausing.Fixes #52955