Skip to content

Commit 92e5222

Browse files
committed
[JSC] Implement await using syntax from Explicit Resource Management proposal
https://bugs.webkit.org/show_bug.cgi?id=309673 Reviewed by Yusuke Suzuki. This patch implements `await using` declarations from the Explicit Resource Management proposal, completing the syntax-level support for the proposal alongside the previously landed synchronous `using` declarations. An `await using` declaration invokes the @@asyncDispose method of a resource when the enclosing scope is exited, awaiting the result. If @@asyncDispose is absent, it falls back to @@dispose wrapped in a promise-returning closure per the spec's GetDisposeMethod(V, async-dispose). This patch extends the parser and bytecode compiler. No new bytecodes are introduced; the existing op_yield / Generatorification machinery handles the suspend points. ** Parser ** The parser recognizes `await [no LineTerminator here] using [no LineTerminator here] BindingList` in contexts where `await` is a keyword: async functions, async generators, and the top level of modules. Both no-LineTerminator constraints are checked via two-token lookahead with savepoint restore. Like sync `using`, it is a SyntaxError at script/eval top level and directly inside switch case/default clauses; destructuring patterns are disallowed. `await using` also appears in for-of heads, both `for (await using x of ...)` and `for await (await using x of ...)`. Unlike sync `using`, the lookahead permits `for (await using of of ...)` where the first `of` is a binding name. Crucially, when an `await using` is seen, the parser calls setUsesAwait() on the current function scope. Without this, the isAsyncFunctionWithoutAwait optimization would inline the body into the wrapper, leaving no generator register for emitAwait to use. A separate boolean m_hasAwaitUsingDeclaration is added to VariableEnvironment (rather than a new Traits bit, as uint16_t m_bits is already full) so the bytecode generator can determine whether any slot in a given scope is async. ** Bytecode Compiler ** The UsingSlot structure gains two fields: a `reached` boolean register and an `isAsync` compile-time flag. Unlike sync `using`, where `method == undefined` alone suffices to skip a slot (covering both "not reached" and "null value"), async requires distinguishing the two because `await using x = null` must still produce exactly one Await(undefined) at disposal time, per the spec's needsAwait/hasAwaited protocol. The reached register is set to true only after @getAsyncDisposeMethod returns successfully; if the method lookup throws, the slot is treated as never reached, avoiding a spurious Await(undefined) before propagating the error. The finally block maintains two runtime registers, needsAwait and hasAwaited, mirroring DisposeResources steps 3-6. These ensure multiple null `await using` declarations produce at most one Await(undefined). A compile-time cursor tracks whether any async slot has been encountered yet in disposal order; Step 3.d checks and the trailing Step 6 check are elided where statically unreachable, reducing unnecessary yield points. For a scope mixing sync and async: { using a = resource1; // sync await using b = resource2; // async } The generated disposal sequence is: // slot[1] (async, disposed first): call b[@@asyncDispose], await result // - if method undefined (null value): needsAwait = true // - if call succeeds: hasAwaited = true; await result // slot[0] (sync): // - Step 3.d: if needsAwait && !hasAwaited, Await(undefined) // - call a[@@dispose] // trailing: elided (slot[0] is sync) ** Built-in ** A new link-time constant @getAsyncDisposeMethod implements GetDisposeMethod with async-dispose hint: it tries @@asyncDispose first, then falls back to @@dispose wrapped in a closure that calls the sync method, discards its return value, and resolves to undefined (or rejects on throw). This replaces the previous @getAsyncDisposableMethod, which lacked the fallback. Tests: JSTests/stress/await-using-declaration-basic.js JSTests/stress/await-using-declaration-error.js JSTests/stress/await-using-declaration-for-of.js JSTests/stress/await-using-declaration-generator.js JSTests/stress/await-using-declaration-mixed.js JSTests/stress/await-using-declaration-syntax-errors.js * JSTests/stress/await-using-declaration-basic.js: Added. (shouldBe): (async test): (async testReverseOrder): (async testNull.promise): (async testMixed): (async testSyncFallback): (async testAsyncDisposeReturnValueAwaited): (async testNotEvaluatedNoAwait.promise): (async main): * JSTests/stress/await-using-declaration-error.js: Added. (shouldBe): (async await): (async testDisposeThrows): (async testDisposeRejects): (async testSuppressedError): (async testBodyThrowThenDisposeThrow): (async testSyncFallbackThrows): (async testMethodLookupThrowNoAwait.async inner): (async main): * JSTests/stress/await-using-declaration-for-of.js: Added. (shouldBe): (async testForOf): (async testForAwaitOf): (async testForOfContinue): (async testForOfBreak): (async main): (main.then): * JSTests/stress/await-using-declaration-generator.js: Added. (shouldBe): (async testAsyncGenerator.async gen): (async testAsyncGeneratorReturn.async gen): (async testAsyncGeneratorNestedBlocks.async gen): (async main): * JSTests/stress/await-using-declaration-mixed.js: Added. (shouldBe): (async testSyncThenAsync): (async testAsyncThenSync): (async testInterleaved): (async testMixedWithNullAwaitUsing.p): (async testMixedSuppressedError): (async main): * JSTests/stress/await-using-declaration-syntax-errors.js: Added. (shouldThrowSyntaxError): (shouldThrowSyntaxError.f): (async shouldThrowSyntaxError): (async shouldNotThrowSyntaxError): * JSTests/test262/config.yaml: * Source/JavaScriptCore/builtins/DisposableStackPrototype.js: (linkTimeConstant.createDisposableResource): (linkTimeConstant.getAsyncDisposeMethod): (linkTimeConstant.getAsyncDisposableMethod): Deleted. * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp: (JSC::BytecodeGenerator::emitPrepareDisposable): (JSC::BytecodeGenerator::emitUsingBodyScope): (JSC::BytecodeGenerator::emitBodyWithUsingIfNeeded): * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h: * Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp: (JSC::initializationModeForAssignmentContext): (JSC::isUsingOrAwaitUsingAssignmentContext): (JSC::AssignResolveNode::emitBytecode): (JSC::BlockNode::emitBytecode): (JSC::ForNode::emitBytecode): (JSC::ForOfNode::emitBytecode): (JSC::SwitchNode::emitBytecode): (JSC::emitProgramNodeBytecode): (JSC::EvalNode::emitBytecode): (JSC::FunctionNode::emitBytecode): (JSC::BindingNode::bindValue const): * Source/JavaScriptCore/parser/ASTBuilder.h: (JSC::ASTBuilder::createAssignResolve): (JSC::ASTBuilder::createBindingLocation): * Source/JavaScriptCore/parser/Nodes.h: (JSC::VariableEnvironmentNode::hasAwaitUsingDeclaration const): * Source/JavaScriptCore/parser/Parser.cpp: (JSC::Parser<LexerType>::parseStatementListItem): (JSC::Parser<LexerType>::parseVariableDeclaration): (JSC::Parser<LexerType>::parseVariableDeclarationList): (JSC::Parser<LexerType>::parseForStatement): * Source/JavaScriptCore/parser/Parser.h: (JSC::Scope::declareLexicalVariable): * Source/JavaScriptCore/parser/VariableEnvironment.cpp: (JSC::VariableEnvironment::swap): * Source/JavaScriptCore/parser/VariableEnvironment.h: (JSC::VariableEnvironment::VariableEnvironment): (JSC::VariableEnvironment::hasAwaitUsingDeclaration const): (JSC::VariableEnvironment::setHasAwaitUsingDeclaration): * Source/JavaScriptCore/runtime/CachedTypes.cpp: (JSC::CachedVariableEnvironment::encode): (JSC::CachedVariableEnvironment::decode const): Canonical link: https://commits.webkit.org/309389@main
1 parent 0bc30db commit 92e5222

18 files changed

Lines changed: 831 additions & 116 deletions
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`bad value: ${String(actual)}, expected: ${String(expected)}`);
6+
}
7+
8+
async function test() {
9+
var order = [];
10+
{
11+
await using a = {
12+
[Symbol.asyncDispose]() {
13+
order.push("dispose-a");
14+
return Promise.resolve();
15+
}
16+
};
17+
order.push("body");
18+
}
19+
order.push("after");
20+
shouldBe(order.join(","), "body,dispose-a,after");
21+
}
22+
23+
async function testReverseOrder() {
24+
var order = [];
25+
{
26+
await using a = { [Symbol.asyncDispose]() { order.push("a"); } };
27+
await using b = { [Symbol.asyncDispose]() { order.push("b"); } };
28+
await using c = { [Symbol.asyncDispose]() { order.push("c"); } };
29+
}
30+
shouldBe(order.join(","), "c,b,a");
31+
}
32+
33+
async function testNull() {
34+
var order = [];
35+
var wasSync = true;
36+
var promise = (async function() {
37+
{
38+
order.push("before");
39+
await using x = null;
40+
order.push("after-decl");
41+
}
42+
order.push("after-block");
43+
shouldBe(wasSync, false);
44+
})();
45+
wasSync = false;
46+
await promise;
47+
shouldBe(order.join(","), "before,after-decl,after-block");
48+
}
49+
50+
async function testMixed() {
51+
var order = [];
52+
{
53+
await using a = { [Symbol.asyncDispose]() { order.push("async-a"); } };
54+
using b = { [Symbol.dispose]() { order.push("sync-b"); } };
55+
order.push("body");
56+
}
57+
shouldBe(order.join(","), "body,sync-b,async-a");
58+
}
59+
60+
async function testSyncFallback() {
61+
var calledAsync = false;
62+
var calledSync = false;
63+
{
64+
await using a = {
65+
[Symbol.dispose]() { calledSync = true; }
66+
};
67+
}
68+
shouldBe(calledAsync, false);
69+
shouldBe(calledSync, true);
70+
}
71+
72+
async function testAsyncDisposeReturnValueAwaited() {
73+
var resolved = false;
74+
{
75+
await using a = {
76+
[Symbol.asyncDispose]() {
77+
return new Promise(resolve => {
78+
Promise.resolve().then(() => {
79+
resolved = true;
80+
resolve();
81+
});
82+
});
83+
}
84+
};
85+
}
86+
shouldBe(resolved, true);
87+
}
88+
89+
async function testNotEvaluatedNoAwait() {
90+
var wasSync = true;
91+
var promise = (async function() {
92+
outer: {
93+
if (true) break outer;
94+
await using x = null;
95+
}
96+
shouldBe(wasSync, true);
97+
})();
98+
wasSync = false;
99+
await promise;
100+
}
101+
102+
async function main() {
103+
await test();
104+
await testReverseOrder();
105+
await testNull();
106+
await testMixed();
107+
await testSyncFallback();
108+
await testAsyncDisposeReturnValueAwaited();
109+
await testNotEvaluatedNoAwait();
110+
}
111+
112+
main().then(() => {}, e => { print("FAIL: " + e); throw e; });
113+
drainMicrotasks();
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`bad value: ${String(actual)}, expected: ${String(expected)}`);
6+
}
7+
8+
function shouldThrowAsync(func, errorCheck) {
9+
let caught = null;
10+
func().then(() => {}, e => { caught = e; });
11+
drainMicrotasks();
12+
if (!caught)
13+
throw new Error("did not throw");
14+
if (errorCheck && !errorCheck(caught))
15+
throw new Error("wrong error: " + caught);
16+
}
17+
18+
shouldThrowAsync(async function() {
19+
await using a = 42;
20+
}, e => e instanceof TypeError);
21+
22+
shouldThrowAsync(async function() {
23+
await using a = {};
24+
}, e => e instanceof TypeError);
25+
26+
shouldThrowAsync(async function() {
27+
await using a = { [Symbol.asyncDispose]: 42 };
28+
}, e => e instanceof TypeError);
29+
30+
async function testDisposeThrows() {
31+
let caught;
32+
try {
33+
await using a = {
34+
[Symbol.asyncDispose]() { throw new Error("dispose error"); }
35+
};
36+
} catch (e) {
37+
caught = e;
38+
}
39+
shouldBe(caught instanceof Error, true);
40+
shouldBe(caught.message, "dispose error");
41+
}
42+
43+
async function testDisposeRejects() {
44+
let caught;
45+
try {
46+
await using a = {
47+
[Symbol.asyncDispose]() { return Promise.reject(new Error("reject error")); }
48+
};
49+
} catch (e) {
50+
caught = e;
51+
}
52+
shouldBe(caught instanceof Error, true);
53+
shouldBe(caught.message, "reject error");
54+
}
55+
56+
async function testSuppressedError() {
57+
let caught;
58+
try {
59+
await using a = { [Symbol.asyncDispose]() { throw new Error("a"); } };
60+
await using b = { [Symbol.asyncDispose]() { throw new Error("b"); } };
61+
} catch (e) {
62+
caught = e;
63+
}
64+
shouldBe(caught instanceof SuppressedError, true);
65+
shouldBe(caught.error.message, "a");
66+
shouldBe(caught.suppressed.message, "b");
67+
}
68+
69+
async function testBodyThrowThenDisposeThrow() {
70+
let caught;
71+
try {
72+
await using a = { [Symbol.asyncDispose]() { throw new Error("dispose"); } };
73+
throw new Error("body");
74+
} catch (e) {
75+
caught = e;
76+
}
77+
shouldBe(caught instanceof SuppressedError, true);
78+
shouldBe(caught.error.message, "dispose");
79+
shouldBe(caught.suppressed.message, "body");
80+
}
81+
82+
async function testSyncFallbackThrows() {
83+
let caught;
84+
try {
85+
await using a = {
86+
[Symbol.dispose]() { throw new Error("sync dispose"); }
87+
};
88+
} catch (e) {
89+
caught = e;
90+
}
91+
shouldBe(caught instanceof Error, true);
92+
shouldBe(caught.message, "sync dispose");
93+
}
94+
95+
async function testMethodLookupThrowNoAwait() {
96+
var order = [];
97+
async function inner() {
98+
order.push("start");
99+
try {
100+
await using x = { [Symbol.asyncDispose]: 42 };
101+
order.push("unreachable");
102+
} catch (e) {
103+
order.push("caught:" + e.constructor.name);
104+
}
105+
order.push("end");
106+
}
107+
var p = inner();
108+
order.push("after-call");
109+
await p;
110+
shouldBe(order.join(","), "start,caught:TypeError,end,after-call");
111+
}
112+
113+
114+
async function main() {
115+
await testDisposeThrows();
116+
await testDisposeRejects();
117+
await testSuppressedError();
118+
await testBodyThrowThenDisposeThrow();
119+
await testSyncFallbackThrows();
120+
await testMethodLookupThrowNoAwait();
121+
}
122+
123+
main().then(() => {}, e => { print("FAIL: " + e); $vm.abort(); });
124+
drainMicrotasks();
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`bad value: ${String(actual)}, expected: ${String(expected)}`);
6+
}
7+
8+
async function testForOf() {
9+
var order = [];
10+
var resources = [
11+
{ id: "a", [Symbol.asyncDispose]() { order.push("dispose-a"); } },
12+
{ id: "b", [Symbol.asyncDispose]() { order.push("dispose-b"); } },
13+
{ id: "c", [Symbol.asyncDispose]() { order.push("dispose-c"); } },
14+
];
15+
for (await using r of resources) {
16+
order.push("body-" + r.id);
17+
}
18+
shouldBe(order.join(","), "body-a,dispose-a,body-b,dispose-b,body-c,dispose-c");
19+
}
20+
21+
async function testForAwaitOf() {
22+
var order = [];
23+
var asyncIterable = {
24+
[Symbol.asyncIterator]() {
25+
var i = 0;
26+
return {
27+
next() {
28+
if (i < 2) {
29+
var id = String.fromCharCode(97 + i);
30+
i++;
31+
return Promise.resolve({ value: { id, [Symbol.asyncDispose]() { order.push("dispose-" + id); } }, done: false });
32+
}
33+
return Promise.resolve({ value: undefined, done: true });
34+
}
35+
};
36+
}
37+
};
38+
for await (await using r of asyncIterable) {
39+
order.push("body-" + r.id);
40+
}
41+
shouldBe(order.join(","), "body-a,dispose-a,body-b,dispose-b");
42+
}
43+
44+
async function testForOfContinue() {
45+
var order = [];
46+
var resources = [
47+
{ id: "a", [Symbol.asyncDispose]() { order.push("dispose-a"); } },
48+
{ id: "b", [Symbol.asyncDispose]() { order.push("dispose-b"); } },
49+
];
50+
for (await using r of resources) {
51+
order.push("pre-" + r.id);
52+
if (r.id === "a") continue;
53+
order.push("post-" + r.id);
54+
}
55+
shouldBe(order.join(","), "pre-a,dispose-a,pre-b,post-b,dispose-b");
56+
}
57+
58+
async function testForOfBreak() {
59+
var order = [];
60+
var resources = [
61+
{ id: "a", [Symbol.asyncDispose]() { order.push("dispose-a"); } },
62+
{ id: "b", [Symbol.asyncDispose]() { order.push("dispose-b"); } },
63+
];
64+
for (await using r of resources) {
65+
order.push("body-" + r.id);
66+
if (r.id === "a") break;
67+
}
68+
shouldBe(order.join(","), "body-a,dispose-a");
69+
}
70+
71+
async function main() {
72+
await testForOf();
73+
await testForAwaitOf();
74+
await testForOfContinue();
75+
await testForOfBreak();
76+
}
77+
78+
main().then(() => {}, e => { print("FAIL: " + e); $vm.abort(); });
79+
drainMicrotasks();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`bad value: ${String(actual)}, expected: ${String(expected)}`);
6+
}
7+
8+
async function testAsyncGenerator() {
9+
var order = [];
10+
async function* gen() {
11+
await using a = { [Symbol.asyncDispose]() { order.push("dispose-a"); } };
12+
yield 1;
13+
yield 2;
14+
order.push("body-end");
15+
}
16+
var iter = gen();
17+
shouldBe((await iter.next()).value, 1);
18+
shouldBe((await iter.next()).value, 2);
19+
shouldBe((await iter.next()).done, true);
20+
shouldBe(order.join(","), "body-end,dispose-a");
21+
}
22+
23+
async function testAsyncGeneratorReturn() {
24+
var order = [];
25+
async function* gen() {
26+
await using a = { [Symbol.asyncDispose]() { order.push("dispose-a"); } };
27+
yield 1;
28+
order.push("never");
29+
}
30+
var iter = gen();
31+
shouldBe((await iter.next()).value, 1);
32+
shouldBe((await iter.return(42)).value, 42);
33+
shouldBe(order.join(","), "dispose-a");
34+
}
35+
36+
async function testAsyncGeneratorNestedBlocks() {
37+
var order = [];
38+
async function* gen() {
39+
{
40+
await using a = { [Symbol.asyncDispose]() { order.push("dispose-a"); } };
41+
{
42+
await using b = { [Symbol.asyncDispose]() { order.push("dispose-b"); } };
43+
yield 1;
44+
}
45+
order.push("between");
46+
}
47+
yield 2;
48+
}
49+
var iter = gen();
50+
shouldBe((await iter.next()).value, 1);
51+
shouldBe((await iter.next()).value, 2);
52+
shouldBe((await iter.next()).done, true);
53+
shouldBe(order.join(","), "dispose-b,between,dispose-a");
54+
}
55+
56+
async function main() {
57+
await testAsyncGenerator();
58+
await testAsyncGeneratorReturn();
59+
await testAsyncGeneratorNestedBlocks();
60+
}
61+
62+
main().then(() => {}, e => { print("FAIL: " + e); $vm.abort(); });
63+
drainMicrotasks();

0 commit comments

Comments
 (0)