Merge main

This commit is contained in:
Chris Tate
2026-10-18 00:50:24 -05:00
17 changed files with 667 additions and 35 deletions
@@ -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.
@@ -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 {
@@ -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;
@@ -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
@@ -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",
@@ -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");
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5904-async-generator-completion.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5905-array-from-async-generator.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5906-array-from-async-generator-js.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5958-asserted-union-receiver.ts"
],
"diags": []
}
+63 -6
View File
@@ -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 {
+2
View File
@@ -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 */
@@ -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<void> {
for (let i = 0; i < n; i++) {
log.push("t" + i);
await null;
}
}
async function run(name: string, body: () => Promise<void>): Promise<void> {
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<number> {
for (const i of this.items) yield i;
}
}
async function main(): Promise<void> {
await run("fall off", async () => {
async function* g(): AsyncGenerator<number> { 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<number, number> { 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<number, void> { 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<number> {}
await g().next().then((r) => log.push(`a:${String(r.value)}/${r.done}`));
});
await run("after done", async () => {
async function* g(): AsyncGenerator<number> {}
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<number, number> { 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<number, number> { 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<number> { 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<number> { 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<number> { 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<number> { 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<number> { 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<number, number> { 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<number, number> { 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<number> { 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<number, number> { 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<number, number> { 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<number, number> { 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<number, number> { 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<number> { 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<string> { 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<number, number> { 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<number, number> { 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<number> {}
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<number> { 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();
@@ -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<void> {
for (let i = 0; i < n; i++) {
log.push("t" + i);
await null;
}
}
async function* nums(n: number): AsyncGenerator<number> {
try {
for (let i = 0; i < n; i++) {
log.push("yield " + i);
yield i;
}
} finally {
log.push("gen finally");
}
}
async function* failing(): AsyncGenerator<number> {
yield 1;
throw new Error("gen failure");
}
async function report(label: string, job: Promise<number[]>): Promise<void> {
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<void>): Promise<void> {
log.length = 0;
const tick = ticker(20);
await body();
await tick;
console.log(name + ": " + log.join(" "));
}
async function main(): Promise<void> {
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();
@@ -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();
@@ -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);
+8
View File
@@ -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,