diff --git a/docs/content/limitations/language-features/async-iteration.mdx b/docs/content/limitations/language-features/async-iteration.mdx index ee570f17d..f18377299 100644 --- a/docs/content/limitations/language-features/async-iteration.mdx +++ b/docs/content/limitations/language-features/async-iteration.mdx @@ -1 +1 @@ -- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises with an optional mapping function; async generator sources, a `thisArg`, and arrays that mix values and promises are unsupported. +- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported. diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1a2e30464..03fa03230 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -8951,6 +8951,12 @@ export class LlEmitter { // own turn is the await (nothing runs in between). const genSettles = this.currentAsyncGenerator && (s.awaited === true || this.finallyStack.length > 0); + // A completion turn that awaits x replaces a return() request's + // already-taken await. + if (this.currentAsyncGenerator && !genSettles) { + this.declare(`declare void @scr_async_gen_return_awaits()`); + B.line(`call void @scr_async_gen_return_awaits()`); + } if (this.currentAsyncGenerator && s.awaited !== true && this.finallyStack.length > 0) { if (this.wasi) this.emitWasiSuspend(null); else { diff --git a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts index 43e8b4959..db60cdf1b 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts @@ -17,9 +17,10 @@ import { locOf } from "../../program.js"; import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js"; import { newFnCtx } from "../function-context.js"; import { lowerArrayCallback } from "./callback-arguments.js"; +import { extractIteratorValue, resultRecordOf, type GenType } from "../lower-generators.js"; -/** `Array.fromAsync(source, mapFn?)` over an array, as an async helper - * with the spec's loop: +/** `Array.fromAsync(source, mapFn?)` over an array or an async generator, + * as an async helper with the spec's loop: * * const out = []; let k = 0; * for await (const v of source) out.push(mapFn ? await mapFn(v, k++) : v); @@ -29,9 +30,10 @@ import { lowerArrayCallback } from "./callback-arguments.js"; * awaited (a promise element through its promise, any other value as one * resolved promise), and the iterator result is awaited again, which the * helper spells as the same microtask hops the `for await` array loop - * takes. A throwing or rejecting mapFn, and a rejected element, close the - * iterator before the returned promise rejects. Null when this is not an - * `Array.fromAsync` call. */ + * takes. An async generator is driven directly: each next() result is + * awaited once and its value is used as is. A throwing or rejecting mapFn, + * and a rejected array element, close the iterator before the returned + * promise rejects. Null when this is not an `Array.fromAsync` call. */ export function lowerArrayFromAsyncCall( lowerer: Lowerer, call: ts.CallExpression, @@ -54,23 +56,18 @@ export function lowerArrayFromAsyncCall( lowerer.badType(call, lowerer.typeOf(call)); const outT = resultT.inner; const source = lowerer.lowerExpr(args[0]!); - if (source.type.kind !== "array") { - if (source.type.kind === "generator" && source.type.async) { - // An async generator's completion settles one microtask later here - // than in Node, which would reorder the returned promise. - lowerer.noLowering( - "Array.fromAsync over an async generator", - args[0]!, - "collect its values with for await", - ); - } + let step: SourceStep; + if (source.type.kind === "array") { + step = arraySourceStep(lowerer, args[0]!, source.type.elem); + } else if (source.type.kind === "generator" && source.type.async) { + step = generatorSourceStep(lowerer, args[0]!, source.type); + } else { lowerer.noLowering( `Array.fromAsync over a value of type '${lowerer.checker.typeToString(lowerer.typeOf(args[0]!))}'`, args[0]!, - "an array is the supported source", + "an array or an async generator is the supported source", ); } - const step = arraySourceStep(lowerer, args[0]!, source.type.elem); let mapper: (IrExpr & { type: IrType & { kind: "func" } }) | null = null; if (args[1]) { const callback = lowerArrayCallback(lowerer, args[1], [step.valueT], outT); @@ -207,19 +204,173 @@ function nonThenable(lowerer: Lowerer, type: IrType): boolean { ); } +interface SourceStep { + /** The value each step hands to the mapping function. */ + valueT: IrType; + loop(source: IrExpr, append: (value: IrExpr) => IrStmt[], loc: SrcLoc): IrStmt[]; +} + +/** The async generator loop: each next() result is awaited, `done` ends + * the loop, and the value goes to the mapping function as is (no further + * await, unlike the async-from-sync array form). A mapping function that + * throws or rejects closes the generator by awaiting return(); a failure + * of that close is dropped, so the mapping error rejects the result. A + * rejected next() does not close the generator and rejects the result on + * the turn Node rejects it. */ +function generatorSourceStep(lowerer: Lowerer, node: ts.Expression, genT: GenType): SourceStep { + if (genT.yieldT.kind === "void") { + lowerer.noLowering("Array.fromAsync over an async generator that never yields", node); + } + if (genT.nextT.kind !== "undefinedT" && genT.nextT.kind !== "dyn") { + lowerer.noLowering( + `Array.fromAsync over an async generator whose yields expect .next(value) ('${lowerer.fmt(genT.nextT)}')`, + node, + ); + } + const recT = resultRecordOf(lowerer, genT); + const promiseT: IrType = { kind: "promise", inner: recT }; + const fieldT = lowerer.shapes.get(recT.shapeId)!.fields.find((f) => f.name === "value")!.type; + return { + valueT: genT.yieldT, + loop(source, append, loc) { + const g = lowerer.declareHiddenLocal("%fromAsyncGenerator", genT); + const needsClose = lowerer.declareHiddenLocal("%fromAsyncNeedsClose", BOOL); + needsClose.mutable = true; + const p = lowerer.declareHiddenLocal("%fromAsyncNext", promiseT); + const r = lowerer.declareHiddenLocal("%fromAsyncResult", recT); + r.mutable = true; + const nextError = lowerer.declareHiddenLocal("%fromAsyncNextError", CAUGHT); + const closeError = lowerer.declareHiddenLocal("%fromAsyncCloseError", CAUGHT); + const gRef = varRef(g.id, genT, loc); + const rRef = varRef(r.id, recT, loc); + const read: IrExpr = { + kind: "recordGet", + obj: rRef, + shapeId: recT.shapeId, + field: "value", + type: fieldT, + loc, + }; + const value = extractIteratorValue(lowerer, genT.yieldT, fieldT, read, loc); + if (!value) { + lowerer.noLowering( + `Array.fromAsync over an async generator yielding '${lowerer.fmt(genT.yieldT)}'`, + node, + ); + } + const setClose = (on: boolean): IrStmt => ({ + kind: "assign", + localId: needsClose.id, + value: boolLit(on, loc), + loc, + }); + const loop: IrStmt = { + kind: "while", + cond: boolLit(true, loc), + body: [ + { + kind: "varDecl", + localId: p.id, + init: { kind: "genResume", mode: "next", gen: gRef, arg: null, type: promiseT, loc }, + loc, + }, + { kind: "varDecl", localId: r.id, init: null, loc }, + // A rejected next() rejects the result two reactions later, as + // the rejected array element does. + { + kind: "tryCatch", + tryBody: [ + { + kind: "assign", + localId: r.id, + value: { kind: "awaitExpr", value: varRef(p.id, promiseT, loc), type: recT, loc }, + loc, + }, + ], + catchBody: [hop(loc), hop(loc), { kind: "rethrow", localId: nextError.id, loc }], + catchLocalId: nextError.id, + finallyBody: null, + loc, + }, + { + kind: "if", + cond: { + kind: "recordGet", + obj: rRef, + shapeId: recT.shapeId, + field: "done", + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setClose(true), + ...append(value!), + setClose(false), + ], + loc, + }; + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: g.id, init: source, loc }, + { kind: "varDecl", localId: needsClose.id, init: boolLit(false, loc), loc }, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: null, + catchLocalId: null, + finallyBody: [ + { + kind: "if", + cond: varRef(needsClose.id, BOOL, loc), + then: [closeGenerator(gRef, promiseT, recT, closeError.id, loc)], + else_: null, + loc, + }, + ], + loc, + }, + ]; + return stmts; + }, + }; +} + +/** Await `gen.return()`, dropping a failure of the close. */ +function closeGenerator( + gen: IrExpr, + promiseT: IrType, + recT: IrType, + errorLocalId: string, + loc: SrcLoc, +): IrStmt { + return { + kind: "tryCatch", + tryBody: [ + { + kind: "exprStmt", + expr: { + kind: "awaitExpr", + value: { kind: "genResume", mode: "return", gen, arg: null, type: promiseT, loc }, + type: recT, + loc, + }, + loc, + }, + ], + catchBody: [], + catchLocalId: errorLocalId, + finallyBody: null, + loc, + }; +} + /** The `for await` array loop (lowerForOf's async-from-sync form): two * hops per element that is not a promise, the element's own await plus * one hop for a promise element, two hops before the final done result, * and one hop to close the iterator when the body completes abruptly. */ -function arraySourceStep( - lowerer: Lowerer, - node: ts.Expression, - elem: IrType, -): { - /** The awaited value each element hands to the mapping function. */ - valueT: IrType; - loop(source: IrExpr, append: (value: IrExpr) => IrStmt[], loc: SrcLoc): IrStmt[]; -} { +function arraySourceStep(lowerer: Lowerer, node: ts.Expression, elem: IrType): SourceStep { const yieldedT = arrayValueType(lowerer, elem); let promiseTag: number | null = null; let valueT = yieldedT; diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 38c728027..d3c5eb1ba 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -59,6 +59,11 @@ export function appendImplicitUndefinedReturn( ): void { if (bodyReturn.kind === "void") return; if (!bodyMayFallThrough(body)) return; + // An async generator that falls off its end completes without awaiting + // anything: its request settles as the body returns, where `return x` + // takes a turn to await x. + let settled = false; + if (lowerer.ctx.isAsync === true && lowerer.ctx.generator) settled = true; // A DYN body that can complete without returning (a JS function whose // guarded return may not run — mustSucceed's `if (typeof fn === // 'function') return fn.apply(...)`): JS completes with undefined — @@ -67,6 +72,7 @@ export function appendImplicitUndefinedReturn( body.push({ kind: "return", value: dynUndefinedExpr(loc), + ...(settled ? { awaited: true as const } : {}), loc, }); return; @@ -74,7 +80,7 @@ export function appendImplicitUndefinedReturn( if (bodyReturn.kind === "union") { const value = lowerer.wrappedUndefined(bodyReturn, loc); if (value) { - body.push({ kind: "return", value, loc }); + body.push({ kind: "return", value, ...(settled ? { awaited: true as const } : {}), loc }); return; } // no undefined arm: the trap below stands in, exactly like non-unions diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 77676f8f2..1a9169afc 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -14001,6 +14001,28 @@ export function lowerUnionProperty( } return null; } + // A class instance the checker sees through a wider union (`(concrete + // as A | B).value`): the value is that class's instance, so read its + // field directly. + if (value.type.kind === "object") { + const className = value.type.className; + const info = lowerer.classes.get(className) ?? null; + const field = expr.name.text; + const fieldType = info?.fields.get(field); + if (fieldType !== undefined) + return lowerer.fieldGetExpr( + { container: "class", obj: value, className, field, fieldType }, + locOf(expr), + expr, + ); + const getter = lowerer.findMethodOn(info, `get:${field}`); + if (getter) + return lowerer.fieldGetExpr( + { container: "accessor", obj: value, className, field, fieldType: getter.sig.ret }, + locOf(expr), + expr, + ); + } if (value.type.kind !== "union") { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index c61920c60..2eda4c4ce 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -32,7 +32,7 @@ export type GenType = IrType & { kind: "generator" }; /** The interned IteratorResult record of a generator type (never null for * a MAPPED generator — mapType required it to intern). */ -function resultRecordOf(lowerer: Lowerer, genT: GenType): IrType & { kind: "record" } { +export function resultRecordOf(lowerer: Lowerer, genT: GenType): IrType & { kind: "record" } { const rec = genResultRecord(genT.yieldT, genT.retT, lowerer.shapes, lowerer.unions); if (!rec) throw new InternalCompilerError("lowerer bug: mapped generator without a result record"); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5904-async-generator-completion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5904-async-generator-completion.ts.json new file mode 100644 index 000000000..dd2338a5b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5904-async-generator-completion.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5904-async-generator-completion.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5905-array-from-async-generator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5905-array-from-async-generator.ts.json new file mode 100644 index 000000000..dd9276926 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5905-array-from-async-generator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5905-array-from-async-generator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5906-array-from-async-generator-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5906-array-from-async-generator-js.js.json new file mode 100644 index 000000000..fc7977463 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5906-array-from-async-generator-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5906-array-from-async-generator-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5958-asserted-union-receiver.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5958-asserted-union-receiver.ts.json new file mode 100644 index 000000000..d294caa65 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5958-asserted-union-receiver.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5958-asserted-union-receiver.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 46175cf82..e6a18072d 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4860,6 +4860,13 @@ struct ScrGen { ScrExcCell async_error; bool settle_queued; bool success_hop_done; + /* A return() request at a yield: the queued job resumes the body after + * the return value's await (return_queued), and the body's completion, + * on that request or a later one, settles without another turn + * (return_awaited). An explicit `return x` during the unwind clears it, + * since that return takes its own turn to await x. */ + bool return_queued; + bool return_awaited; #ifdef SCR_WORKERS bool context_abandoned; #endif @@ -5261,6 +5268,8 @@ void scr_coro_yield_ref(ScrFiber *self, void *v, void (*release)(void *)) { scr_gen_slot_ref(&self->gen->out, v, release); } +void scr_async_gen_return_awaits(void) { scr_gen_self()->return_awaited = false; } + void scr_async_gen_hop_done(void) { ScrGen *g = scr_gen_self(); if (!g->is_async) { @@ -5540,6 +5549,21 @@ static void scr_async_gen_capture_caller_error(ScrGen *g) { if (cell->kind != SCR_EXC_NONE) scr_gen_exc_move(&g->async_error, cell); } +/* Whether the body's latest stop settles its request without a further + * turn: a yield whose await already supplied the value, or a completion + * after a return() request's await. */ +static bool scr_async_gen_settles_now(ScrGen *g) { + if (g->success_hop_done) { + g->success_hop_done = false; + return true; + } + if (g->return_awaited && g->state == SCR_GEN_DONE) { + g->return_awaited = false; + return true; + } + return false; +} + static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { const bool was_done = g->state == SCR_GEN_DONE; if (req->mode == SCR_AG_NEXT) { @@ -5551,6 +5575,13 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { scr_gen_slot_reset(&g->ret); g->ret = req->arg; memset(&req->arg, 0, sizeof req->arg); + if (g->state == SCR_GEN_SUSPENDED) { + /* At a yield, AsyncGeneratorYield awaits the return value before + * the body resumes, so finally blocks run one turn later. */ + g->return_queued = true; + scr_async_gen_schedule_settle(g); + return; + } scr_gen_resume_return(g); } else { scr_gen_exc_move(scr_exc_current_cell(), &req->thrown); @@ -5565,18 +5596,39 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { if (g->state == SCR_GEN_AWAITING) return; /* next() on an already-completed generator produces its undefined done - * result immediately. Yield, body return, and return(value) all pass - * through AsyncGeneratorYield's successful Await step. */ + * result immediately. A yield and return(value) on an unstarted or + * completed generator pass through an Await step; a body that falls off + * its end, or whose `return x` already awaited x, marks its completion + * as settled. */ if (was_done && req->mode == SCR_AG_NEXT) { scr_async_gen_finish_active(g, false); - } else if (g->success_hop_done) { - g->success_hop_done = false; + } else if (scr_async_gen_settles_now(g)) { scr_async_gen_finish_active(g, false); } else { scr_async_gen_schedule_settle(g); } } +/* The return() request's await has taken its turn: resume the body with + * the return completion. */ +static void scr_async_gen_resume_returning(ScrGen *g) { + g->return_queued = false; + g->return_awaited = true; + scr_gen_resume_return(g); + scr_async_gen_capture_caller_error(g); + if (g->async_error.kind != SCR_EXC_NONE) { + scr_async_gen_finish_active(g, true); + } else if (g->state == SCR_GEN_AWAITING) { + return; + } else if (scr_async_gen_settles_now(g)) { + scr_async_gen_finish_active(g, false); + } else { + scr_async_gen_schedule_settle(g); + return; + } + scr_async_gen_start_queued(g); +} + static void scr_async_gen_start_queued(ScrGen *g) { /* Keep the object alive while an immediately-settled request releases * its own retain and the loop inspects the next queue node. */ @@ -5687,6 +5739,12 @@ static void scr_async_gen_job(ScrGen *g) { fputs("scriptc: internal error: invalid async generator settlement job\n", stderr); abort(); } + if (g->return_queued) { + scr_gen_retain(g); + scr_async_gen_resume_returning(g); + scr_gen_release(g); + return; + } scr_async_gen_finish_active(g, false); scr_async_gen_start_queued(g); } @@ -5704,8 +5762,7 @@ static void scr_async_gen_resume_ready(ScrGen *g) { scr_async_gen_finish_active(g, true); scr_async_gen_start_queued(g); } else if (g->state != SCR_GEN_AWAITING) { - if (g->success_hop_done) { - g->success_hop_done = false; + if (scr_async_gen_settles_now(g)) { scr_async_gen_finish_active(g, false); scr_async_gen_start_queued(g); } else { diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index f528b8b8f..2c2c0e953 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -133,6 +133,8 @@ void scr_gen_yield_f64(double v); void scr_gen_yield_bool(bool v); void scr_gen_yield_ref(void *v, void (*release)(void *)); /* moves */ void scr_async_gen_hop_done(void); +/* An async generator's `return x` whose completion turn awaits x. */ +void scr_async_gen_return_awaits(void); void scr_gen_out_f64(ScrGen *g, double v); /* trampoline completion stores */ void scr_gen_out_bool(ScrGen *g, bool v); void scr_gen_out_ref(ScrGen *g, void *v, void (*release)(void *)); /* moves */ diff --git a/tests/corpus/5904-async-generator-completion.ts b/tests/corpus/5904-async-generator-completion.ts new file mode 100644 index 000000000..478aae6e9 --- /dev/null +++ b/tests/corpus/5904-async-generator-completion.ts @@ -0,0 +1,188 @@ +// Async generator completion against a microtask ticker: falling off the end +// and bare returns settle as the body completes, `return x` takes a turn to +// await x, return() at a yield awaits its value before finally blocks run, +// and for await loops end on the turn Node ends them. Node.js prints the +// same. +const log: string[] = []; +async function ticker(n: number): Promise { + for (let i = 0; i < n; i++) { + log.push("t" + i); + await null; + } +} +async function run(name: string, body: () => Promise): Promise { + log.length = 0; + const tick = ticker(14); + await body(); + await tick; + console.log(name + ": " + log.join(" ")); +} +class Source { + readonly items: number[]; + constructor(items: number[]) { + this.items = items; + } + async *values(): AsyncGenerator { + for (const i of this.items) yield i; + } +} +async function main(): Promise { + await run("fall off", async () => { + async function* g(): AsyncGenerator { yield 1; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("return value", async () => { + async function* g(): AsyncGenerator { yield 1; return 5; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("bare return", async () => { + async function* g(): AsyncGenerator { yield 1; return; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("empty body", async () => { + async function* g(): AsyncGenerator {} + await g().next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + }); + await run("after done", async () => { + async function* g(): AsyncGenerator {} + const it = g(); + await it.next(); + log.push("mark"); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("return() at yield", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { log.push("finally"); } return 0; } + const it = g(); + await it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.return(7).then((r) => log.push(`r:${String(r.value)}/${r.done}`)); + }); + await run("return() before start", async () => { + async function* g(): AsyncGenerator { yield 1; return 0; } + await g().return(3).then((r) => log.push(`r:${String(r.value)}/${r.done}`)); + }); + await run("throw at end", async () => { + async function* g(): AsyncGenerator { yield 1; throw new Error("x"); } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`), () => log.push("rejected")); + }); + await run("await before end", async () => { + async function* g(): AsyncGenerator { yield 1; await null; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + }); + await run("for await", async () => { + async function* g(): AsyncGenerator { yield 1; yield 2; } + for await (const v of g()) log.push("v" + v); + log.push("loop done"); + }); + await run("for await break", async () => { + async function* g(): AsyncGenerator { try { yield 1; yield 2; } finally { log.push("cleanup"); } } + for await (const v of g()) { log.push("v" + v); break; } + log.push("loop done"); + }); + await run("queued", async () => { + async function* g(): AsyncGenerator { yield 1; } + const it = g(); + void it.next().then((r) => log.push(`a:${String(r.value)}/${r.done}`)); + void it.next().then((r) => log.push(`b:${String(r.value)}/${r.done}`)); + await it.next().then((r) => log.push(`c:${String(r.value)}/${r.done}`)); + }); + + await run("yield in finally", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { yield 9; log.push("after"); } return 0; } + const it = g(); + await it.next(); + void it.return(7).then((r) => log.push(`r:${r.value}/${r.done}`)); + await it.next().then((r) => log.push(`n:${r.value}/${r.done}`)); + }); + await run("await in finally", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { await null; log.push("cleaned"); } return 0; } + const it = g(); + await it.next(); + await it.return(4).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("throw at yield", async () => { + async function* g(): AsyncGenerator { try { yield 1; } catch { log.push("caught"); } } + const it = g(); + await it.next(); + await it.throw(new Error("x")).then((r) => log.push(`t:${r.value}/${r.done}`)); + }); + await run("return through finally", async () => { + async function* g(): AsyncGenerator { try { yield 1; return 5; } finally { log.push("fin"); } } + const it = g(); + void it.next().then((r) => log.push(`a:${r.value}/${r.done}`)); + await it.next().then((r) => log.push(`b:${r.value}/${r.done}`)); + }); + await run("return promise", async () => { + async function* g(): AsyncGenerator { yield 1; return Promise.resolve(3); } + const it = g(); + void it.next().then((r) => log.push(`a:${r.value}/${r.done}`)); + await it.next().then((r) => log.push(`b:${r.value}/${r.done}`)); + }); + await run("two returns", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { log.push("fin"); } return 0; } + const it = g(); + await it.next(); + void it.return(1).then((r) => log.push(`r1:${r.value}/${r.done}`)); + await it.return(2).then((r) => log.push(`r2:${r.value}/${r.done}`)); + }); + await run("return after done", async () => { + async function* g(): AsyncGenerator { return 1; } + const it = g(); + await it.next(); + log.push("mark"); + await it.return(8).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("nested loops break", async () => { + async function* g(n: number): AsyncGenerator { try { for (let i = 0; i < n; i++) yield i; } finally { log.push("close" + n); } } + for await (const a of g(2)) { + for await (const b of g(3)) { log.push(`${a}${b}`); if (b === 1) break; } + } + log.push("done"); + }); + await run("strings", async () => { + async function* g(): AsyncGenerator { yield "a"; yield "b"; } + const parts: string[] = []; + for await (const s of g()) parts.push(s); + log.push(parts.join("")); + }); + + await run("finally return override", async () => { + async function* g(): AsyncGenerator { try { yield 1; } finally { log.push("fin"); return 42; } } + const it = g(); + await it.next(); + await it.return(7).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("class method", async () => { + const parts: number[] = []; + for await (const v of new Source([1, 2, 3]).values()) parts.push(v); + log.push(parts.join(",")); + }); + await run("return while awaiting", async () => { + async function* g(): AsyncGenerator { try { await null; await null; yield 1; } finally { log.push("fin"); } return 0; } + const it = g(); + void it.next().then((r) => log.push(`n:${r.value}/${r.done}`)); + await it.return(5).then((r) => log.push(`r:${r.value}/${r.done}`)); + }); + await run("throw after done", async () => { + async function* g(): AsyncGenerator {} + const it = g(); + await it.next(); + await it.throw(new Error("late")).then(() => log.push("ok"), (e: unknown) => log.push("rejected " + (e as Error).message)); + }); + await run("rejection inside", async () => { + async function* g(): AsyncGenerator { yield 1; await Promise.reject(new Error("inner")); } + const it = g(); + void it.next().then((r) => log.push(`a:${r.value}/${r.done}`)); + await it.next().then(() => log.push("ok"), (e: unknown) => log.push("rejected " + (e as Error).message)); + }); +} +void main(); diff --git a/tests/corpus/5905-array-from-async-generator.ts b/tests/corpus/5905-array-from-async-generator.ts new file mode 100644 index 000000000..f95fd5327 --- /dev/null +++ b/tests/corpus/5905-array-from-async-generator.ts @@ -0,0 +1,61 @@ +// Array.fromAsync over async generators: plain, mapped, async-mapped, a +// throwing or rejecting mapping function (which closes the generator), a +// generator that throws, an empty generator, and two calls at once, +// interleaved with a microtask ticker. Node.js prints the same. +const log: string[] = []; +async function ticker(n: number): Promise { + for (let i = 0; i < n; i++) { + log.push("t" + i); + await null; + } +} +async function* nums(n: number): AsyncGenerator { + try { + for (let i = 0; i < n; i++) { + log.push("yield " + i); + yield i; + } + } finally { + log.push("gen finally"); + } +} +async function* failing(): AsyncGenerator { + yield 1; + throw new Error("gen failure"); +} +async function report(label: string, job: Promise): Promise { + await job.then( + (a) => log.push(label + " [" + a.join(",") + "]"), + (e: unknown) => log.push(label + " rejected " + (e as Error).message), + ); +} +async function run(name: string, body: () => Promise): Promise { + log.length = 0; + const tick = ticker(20); + await body(); + await tick; + console.log(name + ": " + log.join(" ")); +} +async function main(): Promise { + await run("plain", () => report("plain", Array.fromAsync(nums(3)))); + await run("map", () => report("map", Array.fromAsync(nums(3), (x: number, i: number) => x * 10 + i))); + await run("async map", () => report("async map", Array.fromAsync(nums(2), async (x: number) => x + 1))); + await run("map throws", () => + report("map throws", Array.fromAsync(nums(3), (x: number) => { + if (x === 1) throw new Error("map failure"); + return x; + })), + ); + await run("map rejects", () => + report("map rejects", Array.fromAsync(nums(3), async (x: number) => { + if (x === 1) throw new Error("async map failure"); + return x; + })), + ); + await run("generator throws", () => report("generator throws", Array.fromAsync(failing()))); + await run("empty", () => report("empty", Array.fromAsync(nums(0)))); + await run("two at once", async () => { + await Promise.all([report("a", Array.fromAsync(nums(2))), report("b", Array.fromAsync(nums(1)))]); + }); +} +void main(); diff --git a/tests/corpus/5906-array-from-async-generator-js.js b/tests/corpus/5906-array-from-async-generator-js.js new file mode 100644 index 000000000..fa4d22147 --- /dev/null +++ b/tests/corpus/5906-array-from-async-generator-js.js @@ -0,0 +1,60 @@ +// Array.fromAsync over async generators from JavaScript, with and without a +// mapping function, interleaved with a microtask ticker. Node.js prints the +// same. +const log = []; +async function ticker(n ) { + for (let i = 0; i < n; i++) { + log.push("t" + i); + await null; + } +} +async function* nums(n ) { + try { + for (let i = 0; i < n; i++) { + log.push("yield " + i); + yield i; + } + } finally { + log.push("gen finally"); + } +} +async function* failing() { + yield 1; + throw new Error("gen failure"); +} +async function report(label , job ) { + await job.then( + (a) => log.push(label + " [" + a.join(",") + "]"), + (e ) => log.push(label + " rejected " + (e ).message), + ); +} +async function run(name , body ) { + log.length = 0; + const tick = ticker(20); + await body(); + await tick; + console.log(name + ": " + log.join(" ")); +} +async function main() { + await run("plain", () => report("plain", Array.fromAsync(nums(3)))); + await run("map", () => report("map", Array.fromAsync(nums(3), (x , i ) => x * 10 + i))); + await run("async map", () => report("async map", Array.fromAsync(nums(2), async (x ) => x + 1))); + await run("map throws", () => + report("map throws", Array.fromAsync(nums(3), (x ) => { + if (x === 1) throw new Error("map failure"); + return x; + })), + ); + await run("map rejects", () => + report("map rejects", Array.fromAsync(nums(3), async (x ) => { + if (x === 1) throw new Error("async map failure"); + return x; + })), + ); + await run("generator throws", () => report("generator throws", Array.fromAsync(failing()))); + await run("empty", () => report("empty", Array.fromAsync(nums(0)))); + await run("two at once", async () => { + await Promise.all([report("a", Array.fromAsync(nums(2))), report("b", Array.fromAsync(nums(1)))]); + }); +} +void main(); diff --git a/tests/corpus/5958-asserted-union-receiver.ts b/tests/corpus/5958-asserted-union-receiver.ts new file mode 100644 index 000000000..23a591827 --- /dev/null +++ b/tests/corpus/5958-asserted-union-receiver.ts @@ -0,0 +1,47 @@ +// A class instance asserted to a wider union (`(concrete as A | B).value`) +// keeps its class: field and getter reads go to the known class. +class A { + value: number; + label = "a"; + constructor(value: number) { + this.value = value; + } + describe(): string { + return `A(${this.value})`; + } + get doubled(): number { + return this.value * 2; + } +} +class B { + value: number; + flag = true; + constructor(value: number) { + this.value = value; + } + describe(): string { + return `B(${this.value})`; + } + get doubled(): number { + return this.value * 3; + } +} +class C extends A { + override get doubled(): number { + return this.value * 10; + } +} +type Rec = { value: boolean }; + +const concrete = new A(41); +console.log((concrete as A | B).value + 1, (concrete as A | B).doubled); +console.log((new C(5) as A | B).value, (new C(5) as A | B).doubled, (concrete as A | B).describe()); +console.log((concrete as A | Rec).value, ((concrete as A | B) as A | B).value); +const maybe = (concrete as A | B | undefined)?.value; +console.log(maybe); +function show(x: A): string { + return String((x as A | B).value) + (x satisfies A as A | B).doubled; +} +console.log(show(concrete), show(new C(2))); +const widened = concrete as A | B; +console.log(widened.value, widened instanceof A); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 89b7afa6a..f14d932f3 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1849,6 +1849,9 @@ "5883-os-set-priority.ts": 2380, "5900-float16-array.ts": 3000, "5901-float16-array-js.js": 2900, + "5904-async-generator-completion.ts": 2950, + "5905-array-from-async-generator.ts": 2750, + "5906-array-from-async-generator-js.js": 2950, "5910-set-prototype-of-primitive-object-kinds.ts": 2719, "5911-dyn-class-to-primitive.ts": 2850, "5913-prototype-type-error-messages.ts": 2484, @@ -1857,6 +1860,7 @@ "5951-result-never-widening.ts": 4710, "5955-any-promise-methods.js": 3940, "5956-erased-generic-members.ts": 6730, + "5958-asserted-union-receiver.ts": 2430, "5970-net-connect-host-name.ts": 2400, "5971-buffer-chunks-to-string.ts": 2700, "5972-net-set-encoding-listeners.ts": 2550, @@ -4492,6 +4496,9 @@ "5883-os-set-priority.ts": 3417, "5900-float16-array.ts": 3750, "5901-float16-array-js.js": 3700, + "5904-async-generator-completion.ts": 3800, + "5905-array-from-async-generator.ts": 3500, + "5906-array-from-async-generator-js.js": 3750, "5910-set-prototype-of-primitive-object-kinds.ts": 5033, "5911-dyn-class-to-primitive.ts": 4732, "5913-prototype-type-error-messages.ts": 4068, @@ -4500,6 +4507,7 @@ "5951-result-never-widening.ts": 6120, "5955-any-promise-methods.js": 5120, "5956-erased-generic-members.ts": 8750, + "5958-asserted-union-receiver.ts": 3160, "5970-net-connect-host-name.ts": 2800, "5971-buffer-chunks-to-string.ts": 3100, "5972-net-set-encoding-listeners.ts": 3000,