Skip to content

Commit e70904a

Browse files
authored
Implement deterministic collapse of 'await' in 'await using' (#58929)
1 parent f2f91cc commit e70904a

File tree

57 files changed

+554
-224
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+554
-224
lines changed

src/compiler/factory/emitHelpers.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,10 @@ export const addDisposableResourceHelper: UnscopedEmitHelper = {
14171417
};
14181418

14191419
/**
1420+
* The `s` variable represents two boolean flags from the `DisposeResources` algorithm:
1421+
* - `needsAwait` (`1`) — Indicates that an `await using` for a `null` or `undefined` resource was encountered.
1422+
* - `hasAwaited` (`2`) — Indicates that the algorithm has performed an Await.
1423+
*
14201424
* @internal
14211425
*/
14221426
export const disposeResourcesHelper: UnscopedEmitHelper = {
@@ -1430,17 +1434,22 @@ export const disposeResourcesHelper: UnscopedEmitHelper = {
14301434
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
14311435
env.hasError = true;
14321436
}
1437+
var r, s = 0;
14331438
function next() {
1434-
while (env.stack.length) {
1435-
var rec = env.stack.pop();
1439+
while (r = env.stack.pop()) {
14361440
try {
1437-
var result = rec.dispose && rec.dispose.call(rec.value);
1438-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
1441+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
1442+
if (r.dispose) {
1443+
var result = r.dispose.call(r.value);
1444+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
1445+
}
1446+
else s |= 1;
14391447
}
14401448
catch (e) {
14411449
fail(e);
14421450
}
14431451
}
1452+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
14441453
if (env.hasError) throw env.error;
14451454
}
14461455
return next();

src/testRunner/unittests/evaluation/usingDeclarations.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1810,4 +1810,50 @@ describe("unittests:: evaluation:: usingDeclarations", () => {
18101810
"exit",
18111811
]);
18121812
});
1813+
1814+
it("deterministic collapse of Await", async () => {
1815+
const { main, output } = evaluator.evaluateTypeScript(
1816+
`
1817+
export const output: any[] = [];
1818+
1819+
let asyncId = 0;
1820+
function increment() { asyncId++; }
1821+
1822+
export async function main() {
1823+
// increment asyncId at the top of each turn of the microtask queue
1824+
let pending = Promise.resolve();
1825+
for (let i = 0; i < 10; i++) {
1826+
pending = pending.then(increment);
1827+
}
1828+
1829+
{
1830+
using sync1 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 2
1831+
await using async1 = null, async2 = null;
1832+
using sync2 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 1
1833+
await using async3 = null, async4 = null;
1834+
output.push(asyncId); // asyncId: 0
1835+
}
1836+
1837+
output.push(asyncId); // asyncId: Ideally, 2, but ends up being 4 due to delays imposed by 'await'
1838+
1839+
await pending; // wait for the remaining 'increment' frames to complete.
1840+
}
1841+
1842+
`,
1843+
{ target: ts.ScriptTarget.ES2018 },
1844+
);
1845+
1846+
await main();
1847+
1848+
assert.deepEqual(output, [
1849+
0,
1850+
1,
1851+
2,
1852+
1853+
// This really should be 2, but our transpile introduces an extra `await` by necessity to observe the
1854+
// result of __disposeResources. The process of adopting the result ends up taking two turns of the
1855+
// microtask queue.
1856+
4,
1857+
]);
1858+
});
18131859
});

tests/baselines/reference/awaitUsingDeclarations.1(target=es2015).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
137137
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
138138
env.hasError = true;
139139
}
140+
var r, s = 0;
140141
function next() {
141-
while (env.stack.length) {
142-
var rec = env.stack.pop();
142+
while (r = env.stack.pop()) {
143143
try {
144-
var result = rec.dispose && rec.dispose.call(rec.value);
145-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
144+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
145+
if (r.dispose) {
146+
var result = r.dispose.call(r.value);
147+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
148+
}
149+
else s |= 1;
146150
}
147151
catch (e) {
148152
fail(e);
149153
}
150154
}
155+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
151156
if (env.hasError) throw env.error;
152157
}
153158
return next();

tests/baselines/reference/awaitUsingDeclarations.1(target=es2017).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
128128
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
129129
env.hasError = true;
130130
}
131+
var r, s = 0;
131132
function next() {
132-
while (env.stack.length) {
133-
var rec = env.stack.pop();
133+
while (r = env.stack.pop()) {
134134
try {
135-
var result = rec.dispose && rec.dispose.call(rec.value);
136-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
135+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
136+
if (r.dispose) {
137+
var result = r.dispose.call(r.value);
138+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
139+
}
140+
else s |= 1;
137141
}
138142
catch (e) {
139143
fail(e);
140144
}
141145
}
146+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
142147
if (env.hasError) throw env.error;
143148
}
144149
return next();

tests/baselines/reference/awaitUsingDeclarations.1(target=es2022).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
128128
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
129129
env.hasError = true;
130130
}
131+
var r, s = 0;
131132
function next() {
132-
while (env.stack.length) {
133-
var rec = env.stack.pop();
133+
while (r = env.stack.pop()) {
134134
try {
135-
var result = rec.dispose && rec.dispose.call(rec.value);
136-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
135+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
136+
if (r.dispose) {
137+
var result = r.dispose.call(r.value);
138+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
139+
}
140+
else s |= 1;
137141
}
138142
catch (e) {
139143
fail(e);
140144
}
141145
}
146+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
142147
if (env.hasError) throw env.error;
143148
}
144149
return next();

tests/baselines/reference/awaitUsingDeclarations.1(target=es5).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
164164
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
165165
env.hasError = true;
166166
}
167+
var r, s = 0;
167168
function next() {
168-
while (env.stack.length) {
169-
var rec = env.stack.pop();
169+
while (r = env.stack.pop()) {
170170
try {
171-
var result = rec.dispose && rec.dispose.call(rec.value);
172-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
171+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
172+
if (r.dispose) {
173+
var result = r.dispose.call(r.value);
174+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
175+
}
176+
else s |= 1;
173177
}
174178
catch (e) {
175179
fail(e);
176180
}
177181
}
182+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
178183
if (env.hasError) throw env.error;
179184
}
180185
return next();

tests/baselines/reference/awaitUsingDeclarations.2(target=es2015).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
4646
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
4747
env.hasError = true;
4848
}
49+
var r, s = 0;
4950
function next() {
50-
while (env.stack.length) {
51-
var rec = env.stack.pop();
51+
while (r = env.stack.pop()) {
5252
try {
53-
var result = rec.dispose && rec.dispose.call(rec.value);
54-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
53+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
54+
if (r.dispose) {
55+
var result = r.dispose.call(r.value);
56+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
57+
}
58+
else s |= 1;
5559
}
5660
catch (e) {
5761
fail(e);
5862
}
5963
}
64+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
6065
if (env.hasError) throw env.error;
6166
}
6267
return next();

tests/baselines/reference/awaitUsingDeclarations.2(target=es2017).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
3737
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
3838
env.hasError = true;
3939
}
40+
var r, s = 0;
4041
function next() {
41-
while (env.stack.length) {
42-
var rec = env.stack.pop();
42+
while (r = env.stack.pop()) {
4343
try {
44-
var result = rec.dispose && rec.dispose.call(rec.value);
45-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
44+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
45+
if (r.dispose) {
46+
var result = r.dispose.call(r.value);
47+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
48+
}
49+
else s |= 1;
4650
}
4751
catch (e) {
4852
fail(e);
4953
}
5054
}
55+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
5156
if (env.hasError) throw env.error;
5257
}
5358
return next();

tests/baselines/reference/awaitUsingDeclarations.2(target=es2022).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
3737
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
3838
env.hasError = true;
3939
}
40+
var r, s = 0;
4041
function next() {
41-
while (env.stack.length) {
42-
var rec = env.stack.pop();
42+
while (r = env.stack.pop()) {
4343
try {
44-
var result = rec.dispose && rec.dispose.call(rec.value);
45-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
44+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
45+
if (r.dispose) {
46+
var result = r.dispose.call(r.value);
47+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
48+
}
49+
else s |= 1;
4650
}
4751
catch (e) {
4852
fail(e);
4953
}
5054
}
55+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
5156
if (env.hasError) throw env.error;
5257
}
5358
return next();

tests/baselines/reference/awaitUsingDeclarations.2(target=es5).js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
7373
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
7474
env.hasError = true;
7575
}
76+
var r, s = 0;
7677
function next() {
77-
while (env.stack.length) {
78-
var rec = env.stack.pop();
78+
while (r = env.stack.pop()) {
7979
try {
80-
var result = rec.dispose && rec.dispose.call(rec.value);
81-
if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
80+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
81+
if (r.dispose) {
82+
var result = r.dispose.call(r.value);
83+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
84+
}
85+
else s |= 1;
8286
}
8387
catch (e) {
8488
fail(e);
8589
}
8690
}
91+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
8792
if (env.hasError) throw env.error;
8893
}
8994
return next();

0 commit comments

Comments
 (0)