Skip to content

Commit f025a5b

Browse files
authored
built-in iterators should be disposable (#59633)
1 parent 09a8522 commit f025a5b

22 files changed

+950
-104
lines changed

src/harness/evaluatorImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const sourceFileJs = vpath.combine(vfs.srcFolder, "source.js");
1010

1111
// Define a custom "Symbol" constructor to attach missing built-in symbols without
1212
// modifying the global "Symbol" constructor
13-
const FakeSymbol: SymbolConstructor = ((description?: string) => Symbol(description)) as any;
13+
export const FakeSymbol: SymbolConstructor = ((description?: string) => Symbol(description)) as any;
1414
(FakeSymbol as any).prototype = Symbol.prototype;
1515
for (const key of Object.getOwnPropertyNames(Symbol)) {
1616
Object.defineProperty(FakeSymbol, key, Object.getOwnPropertyDescriptor(Symbol, key)!);

src/lib/esnext.disposable.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/// <reference lib="es2015.symbol" />
2+
/// <reference lib="es2015.iterable" />
3+
/// <reference lib="es2018.asynciterable" />
24

35
interface SymbolConstructor {
46
/**
@@ -165,3 +167,9 @@ interface AsyncDisposableStackConstructor {
165167
readonly prototype: AsyncDisposableStack;
166168
}
167169
declare var AsyncDisposableStack: AsyncDisposableStackConstructor;
170+
171+
interface IteratorObject<T, TReturn, TNext> extends Disposable {
172+
}
173+
174+
interface AsyncIteratorObject<T, TReturn, TNext> extends AsyncDisposable {
175+
}

src/testRunner/unittests/evaluation/awaitUsingDeclarations.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,4 +1874,140 @@ describe("unittests:: evaluation:: awaitUsingDeclarations", () => {
18741874
"catch",
18751875
]);
18761876
});
1877+
1878+
it("deterministic collapse of Await", async () => {
1879+
const { main, output } = evaluator.evaluateTypeScript(
1880+
`
1881+
export const output: any[] = [];
1882+
1883+
let asyncId = 0;
1884+
function increment() { asyncId++; }
1885+
1886+
export async function main() {
1887+
// increment asyncId at the top of each turn of the microtask queue
1888+
let pending = Promise.resolve();
1889+
for (let i = 0; i < 10; i++) {
1890+
pending = pending.then(increment);
1891+
}
1892+
1893+
{
1894+
using sync1 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 2
1895+
await using async1 = null, async2 = null;
1896+
using sync2 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 1
1897+
await using async3 = null, async4 = null;
1898+
output.push(asyncId); // asyncId: 0
1899+
}
1900+
1901+
output.push(asyncId); // asyncId: Ideally, 2, but ends up being 4 due to delays imposed by 'await'
1902+
1903+
await pending; // wait for the remaining 'increment' frames to complete.
1904+
}
1905+
1906+
`,
1907+
{ target: ts.ScriptTarget.ES2018 },
1908+
);
1909+
1910+
await main();
1911+
1912+
assert.deepEqual(output, [
1913+
0,
1914+
1,
1915+
2,
1916+
1917+
// This really should be 2, but our transpile introduces an extra `await` by necessity to observe the
1918+
// result of __disposeResources. The process of adopting the result ends up taking two turns of the
1919+
// microtask queue.
1920+
4,
1921+
]);
1922+
});
1923+
1924+
it("'await using' with downlevel generators", async () => {
1925+
abstract class Iterator {
1926+
return?(): void;
1927+
[evaluator.FakeSymbol.iterator]() {
1928+
return this;
1929+
}
1930+
[evaluator.FakeSymbol.dispose]() {
1931+
this.return?.();
1932+
}
1933+
}
1934+
1935+
const { main } = evaluator.evaluateTypeScript(
1936+
`
1937+
let exited = false;
1938+
1939+
function * f() {
1940+
try {
1941+
yield;
1942+
}
1943+
finally {
1944+
exited = true;
1945+
}
1946+
}
1947+
1948+
export async function main() {
1949+
{
1950+
await using g = f();
1951+
g.next();
1952+
}
1953+
1954+
return exited;
1955+
}
1956+
`,
1957+
{
1958+
target: ts.ScriptTarget.ES5,
1959+
},
1960+
{
1961+
Iterator,
1962+
},
1963+
);
1964+
1965+
const exited = await main();
1966+
assert.isTrue(exited, "Expected 'await using' to dispose generator");
1967+
});
1968+
1969+
it("'await using' with downlevel async generators", async () => {
1970+
abstract class AsyncIterator {
1971+
return?(): PromiseLike<void>;
1972+
[evaluator.FakeSymbol.asyncIterator]() {
1973+
return this;
1974+
}
1975+
async [evaluator.FakeSymbol.asyncDispose]() {
1976+
await this.return?.();
1977+
}
1978+
}
1979+
1980+
const { main } = evaluator.evaluateTypeScript(
1981+
`
1982+
let exited = false;
1983+
1984+
async function * f() {
1985+
try {
1986+
yield;
1987+
}
1988+
finally {
1989+
exited = true;
1990+
}
1991+
}
1992+
1993+
export async function main() {
1994+
{
1995+
await using g = f();
1996+
await g.next();
1997+
}
1998+
1999+
return exited;
2000+
}
2001+
`,
2002+
{
2003+
target: ts.ScriptTarget.ES5,
2004+
},
2005+
{
2006+
AsyncIterator,
2007+
},
2008+
);
2009+
2010+
const exited = await main();
2011+
assert.isTrue(exited, "Expected 'await using' to dispose async generator");
2012+
});
18772013
});

src/testRunner/unittests/evaluation/usingDeclarations.ts

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,49 +1811,48 @@ describe("unittests:: evaluation:: usingDeclarations", () => {
18111811
]);
18121812
});
18131813

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);
1814+
it("'using' with downlevel generators", () => {
1815+
abstract class Iterator {
1816+
return?(): void;
1817+
[evaluator.FakeSymbol.iterator]() {
1818+
return this;
18271819
}
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
1820+
[evaluator.FakeSymbol.dispose]() {
1821+
this.return?.();
18351822
}
1823+
}
18361824

1837-
output.push(asyncId); // asyncId: Ideally, 2, but ends up being 4 due to delays imposed by 'await'
1825+
const { main } = evaluator.evaluateTypeScript(
1826+
`
1827+
let exited = false;
18381828
1839-
await pending; // wait for the remaining 'increment' frames to complete.
1840-
}
1829+
function * f() {
1830+
try {
1831+
yield;
1832+
}
1833+
finally {
1834+
exited = true;
1835+
}
1836+
}
1837+
1838+
export function main() {
1839+
{
1840+
using g = f();
1841+
g.next();
1842+
}
18411843
1844+
return exited;
1845+
}
18421846
`,
1843-
{ target: ts.ScriptTarget.ES2018 },
1847+
{
1848+
target: ts.ScriptTarget.ES5,
1849+
},
1850+
{
1851+
Iterator,
1852+
},
18441853
);
18451854

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-
]);
1855+
const exited = main();
1856+
assert.isTrue(exited, "Expected 'using' to dispose generator");
18581857
});
18591858
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
awaitUsingDeclarationsWithAsyncIteratorObject.ts(11,23): error TS2851: The initializer of an 'await using' declaration must be either an object with a '[Symbol.asyncDispose]()' or '[Symbol.dispose]()' method, or be 'null' or 'undefined'.
2+
3+
4+
==== awaitUsingDeclarationsWithAsyncIteratorObject.ts (1 errors) ====
5+
declare const ai: AsyncIterator<string, undefined>;
6+
declare const aio: AsyncIteratorObject<string, undefined, unknown>;
7+
declare const ag: AsyncGenerator<string, void>;
8+
9+
async function f() {
10+
// should pass
11+
await using it0 = aio;
12+
await using it1 = ag;
13+
14+
// should fail
15+
await using it2 = ai;
16+
~~
17+
!!! error TS2851: The initializer of an 'await using' declaration must be either an object with a '[Symbol.asyncDispose]()' or '[Symbol.dispose]()' method, or be 'null' or 'undefined'.
18+
}
19+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//// [tests/cases/conformance/statements/VariableStatements/usingDeclarations/awaitUsingDeclarationsWithAsyncIteratorObject.ts] ////
2+
3+
=== awaitUsingDeclarationsWithAsyncIteratorObject.ts ===
4+
declare const ai: AsyncIterator<string, undefined>;
5+
>ai : Symbol(ai, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 0, 13))
6+
>AsyncIterator : Symbol(AsyncIterator, Decl(lib.es2018.asynciterable.d.ts, --, --))
7+
8+
declare const aio: AsyncIteratorObject<string, undefined, unknown>;
9+
>aio : Symbol(aio, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 1, 13))
10+
>AsyncIteratorObject : Symbol(AsyncIteratorObject, Decl(lib.es2018.asynciterable.d.ts, --, --), Decl(lib.esnext.disposable.d.ts, --, --))
11+
12+
declare const ag: AsyncGenerator<string, void>;
13+
>ag : Symbol(ag, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 2, 13))
14+
>AsyncGenerator : Symbol(AsyncGenerator, Decl(lib.es2018.asyncgenerator.d.ts, --, --))
15+
16+
async function f() {
17+
>f : Symbol(f, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 2, 47))
18+
19+
// should pass
20+
await using it0 = aio;
21+
>it0 : Symbol(it0, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 6, 15))
22+
>aio : Symbol(aio, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 1, 13))
23+
24+
await using it1 = ag;
25+
>it1 : Symbol(it1, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 7, 15))
26+
>ag : Symbol(ag, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 2, 13))
27+
28+
// should fail
29+
await using it2 = ai;
30+
>it2 : Symbol(it2, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 10, 15))
31+
>ai : Symbol(ai, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 0, 13))
32+
}
33+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//// [tests/cases/conformance/statements/VariableStatements/usingDeclarations/awaitUsingDeclarationsWithAsyncIteratorObject.ts] ////
2+
3+
=== awaitUsingDeclarationsWithAsyncIteratorObject.ts ===
4+
declare const ai: AsyncIterator<string, undefined>;
5+
>ai : AsyncIterator<string, undefined, any>
6+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7+
8+
declare const aio: AsyncIteratorObject<string, undefined, unknown>;
9+
>aio : AsyncIteratorObject<string, undefined, unknown>
10+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11+
12+
declare const ag: AsyncGenerator<string, void>;
13+
>ag : AsyncGenerator<string, void, any>
14+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15+
16+
async function f() {
17+
>f : () => Promise<void>
18+
> : ^^^^^^^^^^^^^^^^^^^
19+
20+
// should pass
21+
await using it0 = aio;
22+
>it0 : AsyncIteratorObject<string, undefined, unknown>
23+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
24+
>aio : AsyncIteratorObject<string, undefined, unknown>
25+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26+
27+
await using it1 = ag;
28+
>it1 : AsyncGenerator<string, void, any>
29+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30+
>ag : AsyncGenerator<string, void, any>
31+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
33+
// should fail
34+
await using it2 = ai;
35+
>it2 : AsyncIterator<string, undefined, any>
36+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
37+
>ai : AsyncIterator<string, undefined, any>
38+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39+
}
40+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
awaitUsingDeclarationsWithIteratorObject.ts(20,23): error TS2851: The initializer of an 'await using' declaration must be either an object with a '[Symbol.asyncDispose]()' or '[Symbol.dispose]()' method, or be 'null' or 'undefined'.
2+
3+
4+
==== awaitUsingDeclarationsWithIteratorObject.ts (1 errors) ====
5+
declare const i: Iterator<string, undefined>;
6+
declare const io: IteratorObject<string, undefined, unknown>;
7+
declare const g: Generator<string, void>;
8+
9+
class MyIterator extends Iterator<string> {
10+
next() { return { done: true, value: undefined }; }
11+
}
12+
13+
async function f() {
14+
// should pass
15+
await using it0 = io;
16+
await using it1 = g;
17+
await using it2 = Iterator.from(i)
18+
await using it3 = new MyIterator();
19+
await using it4 = [].values();
20+
await using it5 = new Map<string, string>().entries();
21+
await using it6 = new Set<string>().keys();
22+
23+
// should fail
24+
await using it7 = i;
25+
~
26+
!!! error TS2851: The initializer of an 'await using' declaration must be either an object with a '[Symbol.asyncDispose]()' or '[Symbol.dispose]()' method, or be 'null' or 'undefined'.
27+
}
28+

0 commit comments

Comments
 (0)