Skip to content

Commit

Permalink
built-in iterators should be disposable (microsoft#59633)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuckton authored Aug 16, 2024
1 parent 09a8522 commit f025a5b
Show file tree
Hide file tree
Showing 22 changed files with 950 additions and 104 deletions.
2 changes: 1 addition & 1 deletion src/harness/evaluatorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const sourceFileJs = vpath.combine(vfs.srcFolder, "source.js");

// Define a custom "Symbol" constructor to attach missing built-in symbols without
// modifying the global "Symbol" constructor
const FakeSymbol: SymbolConstructor = ((description?: string) => Symbol(description)) as any;
export const FakeSymbol: SymbolConstructor = ((description?: string) => Symbol(description)) as any;
(FakeSymbol as any).prototype = Symbol.prototype;
for (const key of Object.getOwnPropertyNames(Symbol)) {
Object.defineProperty(FakeSymbol, key, Object.getOwnPropertyDescriptor(Symbol, key)!);
Expand Down
8 changes: 8 additions & 0 deletions src/lib/esnext.disposable.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/// <reference lib="es2015.symbol" />
/// <reference lib="es2015.iterable" />
/// <reference lib="es2018.asynciterable" />

interface SymbolConstructor {
/**
Expand Down Expand Up @@ -165,3 +167,9 @@ interface AsyncDisposableStackConstructor {
readonly prototype: AsyncDisposableStack;
}
declare var AsyncDisposableStack: AsyncDisposableStackConstructor;

interface IteratorObject<T, TReturn, TNext> extends Disposable {
}

interface AsyncIteratorObject<T, TReturn, TNext> extends AsyncDisposable {
}
136 changes: 136 additions & 0 deletions src/testRunner/unittests/evaluation/awaitUsingDeclarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1874,4 +1874,140 @@ describe("unittests:: evaluation:: awaitUsingDeclarations", () => {
"catch",
]);
});

it("deterministic collapse of Await", async () => {
const { main, output } = evaluator.evaluateTypeScript(
`
export const output: any[] = [];
let asyncId = 0;
function increment() { asyncId++; }
export async function main() {
// increment asyncId at the top of each turn of the microtask queue
let pending = Promise.resolve();
for (let i = 0; i < 10; i++) {
pending = pending.then(increment);
}
{
using sync1 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 2
await using async1 = null, async2 = null;
using sync2 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 1
await using async3 = null, async4 = null;
output.push(asyncId); // asyncId: 0
}
output.push(asyncId); // asyncId: Ideally, 2, but ends up being 4 due to delays imposed by 'await'
await pending; // wait for the remaining 'increment' frames to complete.
}
`,
{ target: ts.ScriptTarget.ES2018 },
);

await main();

assert.deepEqual(output, [
0,
1,
2,

// This really should be 2, but our transpile introduces an extra `await` by necessity to observe the
// result of __disposeResources. The process of adopting the result ends up taking two turns of the
// microtask queue.
4,
]);
});

it("'await using' with downlevel generators", async () => {
abstract class Iterator {
return?(): void;
[evaluator.FakeSymbol.iterator]() {
return this;
}
[evaluator.FakeSymbol.dispose]() {
this.return?.();
}
}

const { main } = evaluator.evaluateTypeScript(
`
let exited = false;
function * f() {
try {
yield;
}
finally {
exited = true;
}
}
export async function main() {
{
await using g = f();
g.next();
}
return exited;
}
`,
{
target: ts.ScriptTarget.ES5,
},
{
Iterator,
},
);

const exited = await main();
assert.isTrue(exited, "Expected 'await using' to dispose generator");
});

it("'await using' with downlevel async generators", async () => {
abstract class AsyncIterator {
return?(): PromiseLike<void>;
[evaluator.FakeSymbol.asyncIterator]() {
return this;
}
async [evaluator.FakeSymbol.asyncDispose]() {
await this.return?.();
}
}

const { main } = evaluator.evaluateTypeScript(
`
let exited = false;
async function * f() {
try {
yield;
}
finally {
exited = true;
}
}
export async function main() {
{
await using g = f();
await g.next();
}
return exited;
}
`,
{
target: ts.ScriptTarget.ES5,
},
{
AsyncIterator,
},
);

const exited = await main();
assert.isTrue(exited, "Expected 'await using' to dispose async generator");
});
});
71 changes: 35 additions & 36 deletions src/testRunner/unittests/evaluation/usingDeclarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1811,49 +1811,48 @@ describe("unittests:: evaluation:: usingDeclarations", () => {
]);
});

it("deterministic collapse of Await", async () => {
const { main, output } = evaluator.evaluateTypeScript(
`
export const output: any[] = [];
let asyncId = 0;
function increment() { asyncId++; }
export async function main() {
// increment asyncId at the top of each turn of the microtask queue
let pending = Promise.resolve();
for (let i = 0; i < 10; i++) {
pending = pending.then(increment);
it("'using' with downlevel generators", () => {
abstract class Iterator {
return?(): void;
[evaluator.FakeSymbol.iterator]() {
return this;
}
{
using sync1 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 2
await using async1 = null, async2 = null;
using sync2 = { [Symbol.dispose]() { output.push(asyncId); } }; // asyncId: 1
await using async3 = null, async4 = null;
output.push(asyncId); // asyncId: 0
[evaluator.FakeSymbol.dispose]() {
this.return?.();
}
}

output.push(asyncId); // asyncId: Ideally, 2, but ends up being 4 due to delays imposed by 'await'
const { main } = evaluator.evaluateTypeScript(
`
let exited = false;
await pending; // wait for the remaining 'increment' frames to complete.
}
function * f() {
try {
yield;
}
finally {
exited = true;
}
}
export function main() {
{
using g = f();
g.next();
}
return exited;
}
`,
{ target: ts.ScriptTarget.ES2018 },
{
target: ts.ScriptTarget.ES5,
},
{
Iterator,
},
);

await main();

assert.deepEqual(output, [
0,
1,
2,

// This really should be 2, but our transpile introduces an extra `await` by necessity to observe the
// result of __disposeResources. The process of adopting the result ends up taking two turns of the
// microtask queue.
4,
]);
const exited = main();
assert.isTrue(exited, "Expected 'using' to dispose generator");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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'.


==== awaitUsingDeclarationsWithAsyncIteratorObject.ts (1 errors) ====
declare const ai: AsyncIterator<string, undefined>;
declare const aio: AsyncIteratorObject<string, undefined, unknown>;
declare const ag: AsyncGenerator<string, void>;

async function f() {
// should pass
await using it0 = aio;
await using it1 = ag;

// should fail
await using it2 = ai;
~~
!!! 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'.
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//// [tests/cases/conformance/statements/VariableStatements/usingDeclarations/awaitUsingDeclarationsWithAsyncIteratorObject.ts] ////

=== awaitUsingDeclarationsWithAsyncIteratorObject.ts ===
declare const ai: AsyncIterator<string, undefined>;
>ai : Symbol(ai, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 0, 13))
>AsyncIterator : Symbol(AsyncIterator, Decl(lib.es2018.asynciterable.d.ts, --, --))

declare const aio: AsyncIteratorObject<string, undefined, unknown>;
>aio : Symbol(aio, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 1, 13))
>AsyncIteratorObject : Symbol(AsyncIteratorObject, Decl(lib.es2018.asynciterable.d.ts, --, --), Decl(lib.esnext.disposable.d.ts, --, --))

declare const ag: AsyncGenerator<string, void>;
>ag : Symbol(ag, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 2, 13))
>AsyncGenerator : Symbol(AsyncGenerator, Decl(lib.es2018.asyncgenerator.d.ts, --, --))

async function f() {
>f : Symbol(f, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 2, 47))

// should pass
await using it0 = aio;
>it0 : Symbol(it0, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 6, 15))
>aio : Symbol(aio, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 1, 13))

await using it1 = ag;
>it1 : Symbol(it1, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 7, 15))
>ag : Symbol(ag, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 2, 13))

// should fail
await using it2 = ai;
>it2 : Symbol(it2, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 10, 15))
>ai : Symbol(ai, Decl(awaitUsingDeclarationsWithAsyncIteratorObject.ts, 0, 13))
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//// [tests/cases/conformance/statements/VariableStatements/usingDeclarations/awaitUsingDeclarationsWithAsyncIteratorObject.ts] ////

=== awaitUsingDeclarationsWithAsyncIteratorObject.ts ===
declare const ai: AsyncIterator<string, undefined>;
>ai : AsyncIterator<string, undefined, any>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

declare const aio: AsyncIteratorObject<string, undefined, unknown>;
>aio : AsyncIteratorObject<string, undefined, unknown>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

declare const ag: AsyncGenerator<string, void>;
>ag : AsyncGenerator<string, void, any>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

async function f() {
>f : () => Promise<void>
> : ^^^^^^^^^^^^^^^^^^^

// should pass
await using it0 = aio;
>it0 : AsyncIteratorObject<string, undefined, unknown>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>aio : AsyncIteratorObject<string, undefined, unknown>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

await using it1 = ag;
>it1 : AsyncGenerator<string, void, any>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>ag : AsyncGenerator<string, void, any>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

// should fail
await using it2 = ai;
>it2 : AsyncIterator<string, undefined, any>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>ai : AsyncIterator<string, undefined, any>
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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'.


==== awaitUsingDeclarationsWithIteratorObject.ts (1 errors) ====
declare const i: Iterator<string, undefined>;
declare const io: IteratorObject<string, undefined, unknown>;
declare const g: Generator<string, void>;

class MyIterator extends Iterator<string> {
next() { return { done: true, value: undefined }; }
}

async function f() {
// should pass
await using it0 = io;
await using it1 = g;
await using it2 = Iterator.from(i)
await using it3 = new MyIterator();
await using it4 = [].values();
await using it5 = new Map<string, string>().entries();
await using it6 = new Set<string>().keys();

// should fail
await using it7 = i;
~
!!! 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'.
}

Loading

0 comments on commit f025a5b

Please sign in to comment.