-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Support using
and await using
declarations
#54505
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.
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
.
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 |
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
using
andawait using
declarations 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
using
declaration 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 using
declaration is similar to ausing
declaration, but instead declares an asynchronously disposable resource. In this case, the value must have a[Symbol.asyncDispose]()
method that will beawait
ed at the end of the block:Disposable Resources
A disposable resource must conform to the
Disposable
interface:While an asynchronously disposable resource must conform to either the
Disposable
interface, or theAsyncDisposable
interface:using
DeclarationsA
using
declaration is a block-scoped declaration with an immutable binding, much likeconst
.As with
const
, ausing
declaration must have an initializer and multipleusing
declarations can be declared in a single statement:No Binding Patterns
However, unlike
const
, ausing
declaration 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 thatx
andy
have no dependencies with respect to disposal order.Allowed Values
When a
using
declaration 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 neithernull
norundefined
, an error is thrown:As each
using
declaration 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
using
be declared?A
using
declaration is legal anywhere aconst
declaration 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
const
declaration 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
SuppressedError
exception. In the case of the above example,e
would beallowing you to walk the entire stack of error suppressesions.
await using
DeclarationsAn
await using
declaration 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 beawait
ed when the resource is disposed:Allowed Values
The resource supplied to an
await using
declaration must either have a[Symbol.asyncDispose]()
method or a[Symbol.dispose]()
method, or be eithernull
orundefined
, otherwise an error is thrown:Please note that while a
[Symbol.asyncDispose]()
method doesn't necessarily need to return aPromise
in 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 missingreturn
statements.Where can an
await using
be declared?Since this functionality depends on the ability to use
await
, anawait using
declaration may only appear in places where anawait
orfor await of
statement might be legal.Implicit
await
at end of BlockIt is important to note that any Block containing an
await using
statement will have an implicitawait
that occurs at the end of that block, as long as theawait using
statement 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.
for
Statementsusing
andawait using
declarations are allowed in the head of afor
statement:In this case, the resource (
res
) is not disposed until either iteration completes (i.e.,res.done
istrue
) or thefor
is exited early due toreturn
,throw
,break
, or a non-localcontinue
.for-in
Statementsusing
andawait using
declarations are not allowed in the head of afor-in
statement:for-of
Statementsusing
andawait using
declarations are allowed in the head of afor-of
statement:In a
for-of
statement, block-scoped bindings are initialized once per each iteration, and thus are disposed at the end of each iteration.for-await-of
StatementsMuch like
for-of
,using
andawait using
may be used in the head of afor-await-of
statement:It is important to note that there is a distinction between the above two statements. A
for-await-of
does not implicitly support asynchronously disposed resources when combined with a synchronoususing
, thus anAsyncIterable<AsyncDisposable>
will require bothawaits
infor await (await using ...
.switch
StatementsA
using
orawait using
declaration may appear in in the statement list of acase
ordefault
clause aswitch
statement. In this case, any resources that are tracked for disposal will be disposed when exiting the CaseBlock:Downlevel Emit
The
using
andawait using
statements are supported down-level as long as the following globals are available at runtime:Symbol.dispose
— To support theDisposable
protocol.Symbol.asyncDispose
— To support theAsyncDisposable
protocol.SuppressedError
— To support the error handling semantics ofusing
andawait using
.Promise
— To supportawait using
.A
using
declaration is transformed into atry-catch-finally
block as follows:The
env_
variable holds the stack of resources added by eachusing
statement, as well as any potential error thrown by any subsequent statements.The emit for an
await using
differs only slightly:For
await using
, we conditionallyawait
the result of the__disposeResources
call. The return value will always be aPromise
if at least oneawait using
declaration was evaluated, even if its initializer wasnull
orundefined
(see Implicitawait
at end of Block, above).Important Considerations
super()
The introduction of a
try-catch-finally
wrapper 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-finally
wrapper 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 thees2015
transform and should not hold up this feature.Modules and top-level
using
This also required similar changes to support top-level
using
in 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
--experimentalDecorators
transform (transforms/legacyDecorators.ts) immediately after thets
transform (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 thets
transform, since it is still essentially TypeScript-specific syntax.Future Work
There is still some open work to finish once this PR has merged, including:
tslib
for the new helpers.try-finally
containing a resource into ausing
.Fixes #52955