mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Merge main
This commit is contained in:
@@ -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");
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5904-async-generator-completion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5905-array-from-async-generator.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5906-array-from-async-generator-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5958-asserted-union-receiver.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user