Skip to content

Commit 4ca7559

Browse files
committed
[JSC] Implement using syntax from Explicit Resource Management proposal
https://bugs.webkit.org/show_bug.cgi?id=308507 Reviewed by Yusuke Suzuki. This patch implements `using` declarations from the Explicit Resource Management proposal. `await using` will be addressed in a subsequent patch. A `using` declaration automatically invokes the @@dispose method of a resource when the enclosing scope is exited. The disposal timing is the same as an existing finally block. This patch primarily modifies the parser and the bytecode compiler. No new bytecodes are introduced. ** Parser ** `using` is not a reserved word. When the parser encounters an unescaped identifier named `using`, it looks ahead several tokens to determine whether it is a using declaration. If so, it is parsed through the existing parseVariableDeclarationList function. Like `const`, a `using` binding is non-assignable and requires an initializer, except in for-of heads. A new IsUsing bit is set on VariableEnvironmentEntry so that the bytecode generator can later determine the number of using declarations in each scope. `using` can also appear in for-of heads, e.g. `for (using a of o)`, which requires somewhat complex disambiguation. For example, `for (using of expr)` must be interpreted as a regular for-of loop rather than a using declaration, per the spec's [lookahead ≠ `using` `of`] constraint. Additionally, writing a `using` declaration directly inside a switch case/default clause without a block is a SyntaxError. ** Bytecode Compiler ** BytecodeGenerator maintains a new stack structure called using scope. Each entry in a using scope is a pair of the resource value and its @@dispose method. Slots are pre-allocated before the try block, with the method initialized to undefined. This ensures that the finally block operates safely even if an initializer throws partway through. As each `using` declaration is encountered within the try block, the value and the dispose method obtained via getDisposeMethod are written into the pre-allocated slots. In the finally block, the slots are iterated in reverse order to invoke @@dispose on each resource. If an error occurs during disposal in the finally block, it is chained with any existing error using SuppressedError, so that all error information is preserved even when multiple disposals fail. SuppressedError is already implemented in JSC, so this patch reuses the existing implementation. For example, given the following using declarations: { using a = resource1; using b = resource2; } The following bytecode is generated: // --- Pre-allocate slots (before try) --- mov slot[0].method, Undefined mov slot[1].method, Undefined mov completionType, Normal // --- try block --- // using a = resource1; mov slot[0].value, resource1 call slot[0].method, getDisposeMethod(resource1) // using b = resource2; mov slot[1].value, resource2 call slot[1].method, getDisposeMethod(resource2) // --- finally block --- mov pendingError, Undefined mov hasError, False mov disposeThrew, False // Record body error if the body threw jnstricteq completionType, Throw, skip_body_error mov pendingError, thrownValue mov hasError, True skip_body_error: // Dispose slot[1] (reverse order: b first) jstricteq slot[1].method, Undefined, skip_slot1 call_ignore_result slot[1].method.call(slot[1].value) jmp skip_slot1 catch_slot1: // Disposal threw jfalse hasError, first_error_slot1 call pendingError, SuppressedError(newError, pendingError) jmp done_slot1 first_error_slot1: mov pendingError, newError mov hasError, True done_slot1: mov disposeThrew, True skip_slot1: // Dispose slot[0] (a) jstricteq slot[0].method, Undefined, skip_slot0 call_ignore_result slot[0].method.call(slot[0].value) jmp skip_slot0 catch_slot0: // Same pattern ... skip_slot0: // Final completion jfalse disposeThrew, no_dispose_error throw pendingError // Throw disposal error no_dispose_error: jnstricteq completionType, Throw, done throw originalError // Re-throw body error done: ret * JSTests/stress/using-declaration-basic.js: Added. (shouldBe): (shouldThrow): (throw.new.Error): (using.a.Symbol.dispose): (using.b.Symbol.dispose): (using.c.Symbol.dispose): (shouldBe.order.join.test): (shouldBe.order.join): * JSTests/stress/using-declaration-error.js: Added. (shouldBe): (throw.new.Error): * JSTests/stress/using-declaration-for-of.js: Added. (shouldBe): (throw.new.Error): * JSTests/stress/using-declaration-syntax-errors.js: Added. (shouldThrowSyntaxError): (shouldThrowSyntaxError.string_appeared_here.shouldThrowSyntaxError): * JSTests/test262/config.yaml: * Source/JavaScriptCore/builtins/DisposableStackPrototype.js: (linkTimeConstant.disposeResources): * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp: (JSC::BytecodeGenerator::emitAddDisposableResource): (JSC::BytecodeGenerator::emitDisposeResources): (JSC::BytecodeGenerator::emitUsingBodyScope): * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h: (JSC::BytecodeGenerator::pushDisposeCapability): (JSC::BytecodeGenerator::currentDisposeCapability): (JSC::BytecodeGenerator::popDisposeCapability): * Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp: (JSC::initializationModeForAssignmentContext): (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/Nodes.h: (JSC::VariableEnvironmentNode::hasUsingDeclaration const): * Source/JavaScriptCore/parser/Parser.cpp: (JSC::Parser<LexerType>::parseStatementListItem): (JSC::Parser<LexerType>::parseVariableDeclaration): (JSC::Parser<LexerType>::parseVariableDeclarationList): (JSC::Parser<LexerType>::parseForStatement): (JSC::Parser<LexerType>::parseSwitchClauses): (JSC::Parser<LexerType>::parseSwitchDefaultClause): (JSC::Parser<LexerType>::parseBlockStatement): * Source/JavaScriptCore/parser/Parser.h: (JSC::Scope::takeLexicalEnvironment): (JSC::Scope::declareLexicalVariable): (JSC::Scope::setHasUsingDeclaration): (JSC::Scope::hasUsingDeclaration const): (JSC::Parser::destructuringKindFromDeclarationType): (JSC::Parser::declarationTypeToVariableKind): (JSC::Parser::assignmentContextFromDeclarationType): (JSC::Parser::popScopeInternal): (JSC::Parser::declareVariable): * Source/JavaScriptCore/parser/VariableEnvironment.cpp: (JSC::VariableEnvironment::swap): * Source/JavaScriptCore/parser/VariableEnvironment.h: (JSC::VariableEnvironmentEntry::isUsing const): (JSC::VariableEnvironmentEntry::setIsUsing): (JSC::VariableEnvironment::VariableEnvironment): (JSC::VariableEnvironment::hasUsingDeclaration const): (JSC::VariableEnvironment::setHasUsingDeclaration): * Source/JavaScriptCore/runtime/CachedTypes.cpp: (JSC::CachedVariableEnvironment::encode): (JSC::CachedVariableEnvironment::decode const): * Source/JavaScriptCore/runtime/CommonIdentifiers.cpp: (JSC::CommonIdentifiers::CommonIdentifiers): * Source/JavaScriptCore/runtime/CommonIdentifiers.h: Canonical link: https://commits.webkit.org/308955@main
1 parent dfddac0 commit 4ca7559

28 files changed

Lines changed: 1215 additions & 101 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
.directory
66
/WebKitBuild/
77
/.clangd
8+
.claude/agents/
89
/update-compile-commands-symlink.conf
910
/test262-results/
1011
autoinstall.cache.d
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
//@ runDefault
3+
4+
function shouldBe(actual, expected) {
5+
if (actual !== expected)
6+
throw new Error(`Expected ${expected} but got ${actual}`);
7+
}
8+
9+
async function test() {
10+
{
11+
let order = [];
12+
async function f() {
13+
using x = { [Symbol.dispose]() { order.push("dispose"); } };
14+
order.push("before-await");
15+
await Promise.resolve();
16+
order.push("after-await");
17+
}
18+
await f();
19+
shouldBe(order.join(","), "before-await,after-await,dispose");
20+
}
21+
22+
{
23+
let order = [];
24+
async function f() {
25+
using a = { [Symbol.dispose]() { order.push("a"); } };
26+
await Promise.resolve();
27+
using b = { [Symbol.dispose]() { order.push("b"); } };
28+
return "done";
29+
}
30+
shouldBe(await f(), "done");
31+
shouldBe(order.join(","), "b,a");
32+
}
33+
34+
{
35+
let order = [];
36+
let f = async () => {
37+
using x = { [Symbol.dispose]() { order.push("arrow-dispose"); } };
38+
await Promise.resolve();
39+
};
40+
await f();
41+
shouldBe(order.join(","), "arrow-dispose");
42+
}
43+
}
44+
45+
test().catch(e => {
46+
print("FAIL: " + e.message);
47+
$vm.abort();
48+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`Expected ${expected} but got ${actual}`);
6+
}
7+
8+
{
9+
let disposed = false;
10+
{
11+
using x = { [Symbol.dispose]() { disposed = true; } };
12+
shouldBe(disposed, false);
13+
}
14+
shouldBe(disposed, true);
15+
}
16+
17+
{
18+
{
19+
using x = null;
20+
using y = undefined;
21+
}
22+
}
23+
24+
{
25+
let order = [];
26+
{
27+
using a = { [Symbol.dispose]() { order.push("a"); } };
28+
using b = { [Symbol.dispose]() { order.push("b"); } };
29+
using c = { [Symbol.dispose]() { order.push("c"); } };
30+
}
31+
shouldBe(order.join(","), "c,b,a");
32+
}
33+
34+
{
35+
let disposed = false;
36+
function test() {
37+
using x = { [Symbol.dispose]() { disposed = true; } };
38+
shouldBe(disposed, false);
39+
}
40+
test();
41+
shouldBe(disposed, true);
42+
}
43+
44+
{
45+
let value;
46+
{
47+
using x = { val: 42, [Symbol.dispose]() {} };
48+
value = x.val;
49+
}
50+
shouldBe(value, 42);
51+
}
52+
53+
{
54+
let disposed = false;
55+
try {
56+
using x = { [Symbol.dispose]() { disposed = true; } };
57+
throw new Error("test");
58+
} catch (e) {
59+
shouldBe(e.message, "test");
60+
}
61+
shouldBe(disposed, true);
62+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`Expected ${expected} but got ${actual}`);
6+
}
7+
8+
{
9+
let caught;
10+
try {
11+
{
12+
using a = { [Symbol.dispose]() { throw new Error("a"); } };
13+
using b = { [Symbol.dispose]() { throw new Error("b"); } };
14+
throw new Error("body");
15+
}
16+
} catch (e) {
17+
caught = e;
18+
}
19+
shouldBe(caught instanceof SuppressedError, true);
20+
shouldBe(caught.error.message, "a");
21+
shouldBe(caught.suppressed instanceof SuppressedError, true);
22+
shouldBe(caught.suppressed.error.message, "b");
23+
shouldBe(caught.suppressed.suppressed.message, "body");
24+
}
25+
26+
{
27+
let caught;
28+
try {
29+
{
30+
using a = { [Symbol.dispose]() {} };
31+
using b = { [Symbol.dispose]() { throw new Error("b"); } };
32+
throw new Error("body");
33+
}
34+
} catch (e) {
35+
caught = e;
36+
}
37+
shouldBe(caught instanceof SuppressedError, true);
38+
shouldBe(caught.error.message, "b");
39+
shouldBe(caught.suppressed.message, "body");
40+
}
41+
42+
{
43+
let order = [];
44+
try {
45+
{
46+
using a = { [Symbol.dispose]() { order.push("a-ok"); } };
47+
using b = { [Symbol.dispose]() { order.push("b-ok"); } };
48+
throw new Error("body");
49+
}
50+
} catch (e) {
51+
shouldBe(e.message, "body");
52+
}
53+
shouldBe(order.join(","), "b-ok,a-ok");
54+
}
55+
56+
{
57+
let caught;
58+
try {
59+
{
60+
using a = { [Symbol.dispose]() { throw new Error("a"); } };
61+
using b = { [Symbol.dispose]() { throw new Error("b"); } };
62+
using c = { [Symbol.dispose]() { throw new Error("c"); } };
63+
}
64+
} catch (e) {
65+
caught = e;
66+
}
67+
shouldBe(caught instanceof SuppressedError, true);
68+
shouldBe(caught.error.message, "a");
69+
shouldBe(caught.suppressed instanceof SuppressedError, true);
70+
shouldBe(caught.suppressed.error.message, "b");
71+
shouldBe(caught.suppressed.suppressed.message, "c");
72+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`Expected ${expected} but got ${actual}`);
6+
}
7+
8+
{
9+
let caught;
10+
try {
11+
{
12+
using x = { [Symbol.dispose]() { throw new Error("dispose"); } };
13+
}
14+
} catch (e) {
15+
caught = e;
16+
}
17+
shouldBe(caught instanceof Error, true);
18+
shouldBe(caught.message, "dispose");
19+
}
20+
21+
{
22+
let caught;
23+
try {
24+
{
25+
using a = { [Symbol.dispose]() { throw new Error("a"); } };
26+
using b = { [Symbol.dispose]() { throw new Error("b"); } };
27+
}
28+
} catch (e) {
29+
caught = e;
30+
}
31+
shouldBe(caught instanceof SuppressedError, true);
32+
shouldBe(caught.error.message, "a");
33+
shouldBe(caught.suppressed.message, "b");
34+
}
35+
36+
{
37+
let caught;
38+
try {
39+
{
40+
using x = { [Symbol.dispose]: "not a function" };
41+
}
42+
} catch (e) {
43+
caught = e;
44+
}
45+
shouldBe(caught instanceof TypeError, true);
46+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`Expected ${expected} but got ${actual}`);
6+
}
7+
8+
{
9+
let disposed = false;
10+
eval(`
11+
{
12+
using x = { [Symbol.dispose]() { disposed = true; } };
13+
}
14+
`);
15+
shouldBe(disposed, true);
16+
}
17+
18+
{
19+
let order = [];
20+
eval(`
21+
{
22+
using a = { [Symbol.dispose]() { order.push("a"); } };
23+
using b = { [Symbol.dispose]() { order.push("b"); } };
24+
}
25+
`);
26+
shouldBe(order.join(","), "b,a");
27+
}
28+
29+
{
30+
let caught;
31+
try {
32+
eval(`
33+
{
34+
using x = { [Symbol.dispose]() { throw new Error("eval-dispose"); } };
35+
}
36+
`);
37+
} catch (e) {
38+
caught = e;
39+
}
40+
shouldBe(caught instanceof Error, true);
41+
shouldBe(caught.message, "eval-dispose");
42+
}
43+
44+
{
45+
let caught;
46+
try {
47+
eval(`using x = { [Symbol.dispose]() {} };`);
48+
} catch (e) {
49+
caught = e;
50+
}
51+
shouldBe(caught instanceof SyntaxError, true);
52+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`Expected ${expected} but got ${actual}`);
6+
}
7+
8+
function shouldThrowSyntaxError(code) {
9+
let threw = false;
10+
try {
11+
eval(code);
12+
} catch (e) {
13+
threw = true;
14+
if (!(e instanceof SyntaxError))
15+
throw new Error(`Expected SyntaxError but got ${e.constructor.name}: ${e.message}`);
16+
}
17+
if (!threw)
18+
throw new Error(`Expected SyntaxError for: ${code}`);
19+
}
20+
21+
{
22+
var using;
23+
for (using of [10, 20, 30]) {}
24+
shouldBe(using, 30);
25+
}
26+
27+
{
28+
var using;
29+
var of = [10, 20, 30];
30+
for (using of of) {}
31+
shouldBe(using, 30);
32+
}
33+
34+
{
35+
var using;
36+
var results = [];
37+
for (using of [1, 2, 3]) {
38+
results.push(using);
39+
}
40+
shouldBe(results.join(","), "1,2,3");
41+
}
42+
43+
shouldThrowSyntaxError("for (using x; false; ) {}");
44+
45+
shouldThrowSyntaxError("for (using x = null, y; false; ) {}");
46+
47+
shouldThrowSyntaxError("for (using let of []) {}");
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//@ requireOptions("--useExplicitResourceManagement=true")
2+
3+
function shouldBe(actual, expected) {
4+
if (actual !== expected)
5+
throw new Error(`Expected ${expected} but got ${actual}`);
6+
}
7+
8+
{
9+
let order = [];
10+
for (let i = 0; i < 3; i++) {
11+
using x = { val: i, [Symbol.dispose]() { order.push("dispose-" + this.val); } };
12+
order.push("use-" + x.val);
13+
}
14+
shouldBe(order.join(","), "use-0,dispose-0,use-1,dispose-1,use-2,dispose-2");
15+
}
16+
17+
{
18+
let order = [];
19+
for (let i = 0; i < 3; i++) {
20+
using x = { val: i, [Symbol.dispose]() { order.push("dispose-" + this.val); } };
21+
if (i === 1) break;
22+
order.push("use-" + x.val);
23+
}
24+
shouldBe(order.join(","), "use-0,dispose-0,dispose-1");
25+
}
26+
27+
{
28+
let order = [];
29+
for (let i = 0; i < 3; i++) {
30+
using x = { val: i, [Symbol.dispose]() { order.push("dispose-" + this.val); } };
31+
if (i === 1) continue;
32+
order.push("use-" + x.val);
33+
}
34+
shouldBe(order.join(","), "use-0,dispose-0,dispose-1,use-2,dispose-2");
35+
}

0 commit comments

Comments
 (0)