Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

built-in iterators should be disposable #59633

Merged
merged 2 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these tests of existing functionality? I don't see how changing the type of [Async]IteratorObject would change its runtime behaviour.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was in the wrong test suite. I merely moved while adding other tests.

`
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