mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Close async generators on early for await exits and iterate untyped native streams
This commit is contained in:
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v28");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v29");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -972,6 +972,11 @@ export class LlEmitter {
|
||||
get usesDynBuiltinStream(): boolean {
|
||||
return this.runtimeFeatures.dynBuiltinStream;
|
||||
}
|
||||
/** A checked-dynamic for-await may hold a native stream: the program
|
||||
* links the stream unit (and the dyn-async one the loop needs). */
|
||||
get dynForAwaitStreams(): boolean {
|
||||
return this.runtimeFeatures.stream && this.runtimeFeatures.dynAsync;
|
||||
}
|
||||
/** Runtime-arm dispatchers for live union values. */
|
||||
readonly liveDynUnionRefAdapters = new Map<string, string>();
|
||||
readonly dynPromiseAdapters = new Map<string, string>();
|
||||
|
||||
@@ -553,7 +553,11 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
// which also takes those streams.
|
||||
e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_unbox"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
: // for await over a checked-dynamic value iterates a native
|
||||
// stream behind it (scr_dyn_for_await_stream.c)
|
||||
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
|
||||
? "scr_stream_dyn_async_iterator_open"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
||||
? emitBorrowedInputs(host, e.args)
|
||||
|
||||
@@ -281,6 +281,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"),
|
||||
// for await over checked-dynamic values.
|
||||
unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"),
|
||||
// for await over checked-dynamic values that may hold native streams.
|
||||
unit(
|
||||
(u) => u.opts.dynForAwait === true && u.opts.stream && u.opts.dynAsync,
|
||||
"scr_dyn_for_await_stream.c",
|
||||
),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 28 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const;
|
||||
export const RUNTIME_ABI_VERSION = 29 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v29" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -359,13 +359,15 @@ function boolRef(localId: string, loc: ReturnType<typeof locOf>): IrExpr {
|
||||
|
||||
/** The loop's stream: a JavaScript value typed as a stream can arrive as a
|
||||
* checked-dynamic value (returned through an untyped function), which
|
||||
* checks back into the stream class. */
|
||||
* checks back into the stream class, or as a union with it (an element
|
||||
* read), which narrows to it. */
|
||||
function streamSource(
|
||||
lowerer: Lowerer,
|
||||
plan: NodeReadablePlan,
|
||||
loc: ReturnType<typeof locOf>,
|
||||
): IrExpr {
|
||||
const v = lowerer.lowerExpr(plan.source);
|
||||
if (v.type.kind === "union") return lowerer.coerceToExpected(v, plan.type);
|
||||
if (v.type.kind !== "dyn") return v;
|
||||
return { kind: "dynCheck", value: v, type: plan.type, loc };
|
||||
}
|
||||
|
||||
@@ -4338,6 +4338,21 @@ export function lowerDynDispatchMethodCall(
|
||||
lowerer.chainBlocked(call, access)
|
||||
)
|
||||
return null;
|
||||
// readable.compose lowers its stages from their syntax, so it needs the
|
||||
// stream's class: a stream a JavaScript function returns untyped has none.
|
||||
if (method === "compose") {
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (
|
||||
recvT?.kind === "object" &&
|
||||
streamSidesOf(lowerer, lowerer.classes.get(recvT.className)) !== null
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
"readable.compose on a stream held as a checked-dynamic value",
|
||||
call,
|
||||
"type the stream where it comes from, such as a JSDoc @returns {Readable} on the function that returns it",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (call.arguments.nodes.some((arg) => ts.isSpreadElement(arg))) {
|
||||
const loc = locOf(call);
|
||||
const local = lowerer.declareHiddenLocal("%spreadReceiver", DYN);
|
||||
|
||||
@@ -687,8 +687,12 @@ export function lowerForAwaitGenerator(
|
||||
lowerer.scopes.push(new Map());
|
||||
try {
|
||||
const g = lowerer.declareHiddenLocal("%fag", genT);
|
||||
const done = lowerer.declareHiddenLocal("%fagdone", BOOL);
|
||||
done.mutable = true;
|
||||
// Whether a value has arrived and the body holds the generator open:
|
||||
// leaving the loop then (break, return, a throw, or a return() at a
|
||||
// yield in the body) closes it, as AsyncIteratorClose does; an
|
||||
// exhausted generator and a rejected next() are not closed.
|
||||
const active = lowerer.declareHiddenLocal("%fagactive", BOOL);
|
||||
active.mutable = true;
|
||||
const p = lowerer.declareHiddenLocal("%fagpromise", promiseT);
|
||||
const r = lowerer.declareHiddenLocal("%fagresult", recT);
|
||||
const gRef = (): IrExpr => ({ kind: "varRef", localId: g.id, type: genT, loc });
|
||||
@@ -709,7 +713,14 @@ export function lowerForAwaitGenerator(
|
||||
`for-await over an async generator yielding '${lowerer.fmt(genT.yieldT)}' (no per-element extraction exists)`,
|
||||
);
|
||||
}
|
||||
const setActive = (value: boolean): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: active.id,
|
||||
value: { kind: "boolLit", value, type: BOOL, loc },
|
||||
loc,
|
||||
});
|
||||
const head: IrStmt[] = [
|
||||
setActive(false),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: p.id,
|
||||
@@ -737,19 +748,12 @@ export function lowerForAwaitGenerator(
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: done.id,
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "break", loc },
|
||||
],
|
||||
then: [{ kind: "break", loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
...generatorLoopBinding(lowerer, stmt, extracted, genT.yieldT),
|
||||
setActive(true),
|
||||
];
|
||||
const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels);
|
||||
const closePromise: IrExpr = {
|
||||
@@ -766,34 +770,40 @@ export function lowerForAwaitGenerator(
|
||||
{ kind: "varDecl", localId: g.id, init: iterable, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: done.id,
|
||||
localId: active.id,
|
||||
init: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [...head, ...body],
|
||||
...(labels && { labels }),
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: { kind: "varRef", localId: done.id, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
kind: "tryCatch",
|
||||
tryBody: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "awaitExpr", value: closePromise, type: recT, loc },
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [...head, ...body],
|
||||
...(labels && { labels }),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
// After a throw, an error from closing the generator is dropped.
|
||||
dropFinallyErrors: true,
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "varRef", localId: active.id, type: BOOL, loc },
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "awaitExpr", value: closePromise, type: recT, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3944,6 +3944,29 @@ function lowerStreamCompose(lowerer: Lowerer, args: readonly ts.Expression[], lo
|
||||
/* ── the Readable operators ──────────────────────────────────────────── */
|
||||
|
||||
/** The Readable operators (scr_stream_ops.c — Node's operators.js). */
|
||||
/** A stream member's receiver, lowered: a value the checker types as a
|
||||
* stream class whose lowering is checked-dynamic (a JavaScript binding
|
||||
* that holds an untyped call's result) or a union (an element read)
|
||||
* converts to that class, checked where the member runs. */
|
||||
function streamReceiverExpr(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind !== "dyn" && value.type.kind !== "union") return value;
|
||||
let classT: IrType | null;
|
||||
try {
|
||||
classT = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
if (
|
||||
classT?.kind !== "object" ||
|
||||
streamSidesOf(lowerer, lowerer.classes.get(classT.className)) === null
|
||||
)
|
||||
return value;
|
||||
return value.type.kind === "dyn"
|
||||
? { kind: "dynCheck", value, type: classT, loc: value.loc }
|
||||
: lowerer.coerceToExpected(value, classT);
|
||||
}
|
||||
|
||||
export const STREAM_OPERATORS: ReadonlySet<string> = new Set([
|
||||
"map",
|
||||
"filter",
|
||||
@@ -4066,7 +4089,7 @@ function lowerStreamOperator(
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const args = call.arguments.nodes;
|
||||
const recv = lowerer.lowerExpr(access.expression);
|
||||
const recv = streamReceiverExpr(lowerer, access.expression);
|
||||
const readable: IrType = { kind: "object", className: "%Readable" };
|
||||
const opts = (i: number): IrExpr =>
|
||||
lowerOperatorValue(lowerer, args[i], `${member} options`, loc, true);
|
||||
@@ -4210,7 +4233,7 @@ function pipeIntoStdio(
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
// JS order: the receiver, then the destination, then the options.
|
||||
const receiverValue = lowerer.lowerExpr(access.expression);
|
||||
const receiverValue = streamReceiverExpr(lowerer, access.expression);
|
||||
const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
|
||||
const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type);
|
||||
const options = call.arguments.nodes[1];
|
||||
@@ -4332,7 +4355,7 @@ function pipeIntoDyn(
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
noteUntypedStreamFlow(lowerer, null);
|
||||
const receiverValue = lowerer.lowerExpr(access.expression);
|
||||
const receiverValue = streamReceiverExpr(lowerer, access.expression);
|
||||
const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
|
||||
const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type);
|
||||
const stmts: IrStmt[] = [
|
||||
@@ -4621,7 +4644,7 @@ export function lowerStreamMethodCall(
|
||||
loc,
|
||||
);
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const chunk = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -4637,7 +4660,7 @@ export function lowerStreamMethodCall(
|
||||
if ((kind === "null" || kind === "union" || kind === "dyn") && member === "unshift") {
|
||||
// unshift(value): an object-mode stream's value at the front, null
|
||||
// the end of the stream, as Node's readableAddChunk takes them
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -4647,7 +4670,7 @@ export function lowerStreamMethodCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
if (kind === "null") {
|
||||
return { kind: "libCall", fn: "readable.pushNull", args: [receiver], type: BOOL, loc };
|
||||
}
|
||||
@@ -4700,7 +4723,7 @@ export function lowerStreamMethodCall(
|
||||
if (member === "read") {
|
||||
requireSide(readableSide, "read");
|
||||
if (args.length > 1) lowerer.noLowering(`read with ${args.length} arguments`, call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const size = args[0] ? lowerer.lowerExprExpecting(args[0], F64) : numLit(-1, loc);
|
||||
const type: IrType = unionOf(lowerer, [BYTES, { kind: "nullT" }]);
|
||||
const read: IrExpr = {
|
||||
@@ -4716,7 +4739,7 @@ export function lowerStreamMethodCall(
|
||||
if (member === "pause" || member === "resume") {
|
||||
requireSide(readableSide, member);
|
||||
if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: member === "pause" ? "readable.pause" : "readable.resume",
|
||||
@@ -4729,7 +4752,7 @@ export function lowerStreamMethodCall(
|
||||
if (member === "isPaused") {
|
||||
requireSide(readableSide, "isPaused");
|
||||
if (args.length !== 0) lowerer.noLowering(`isPaused with arguments`, call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
return { kind: "libCall", fn: "readable.isPaused", args: [receiver], type: BOOL, loc };
|
||||
}
|
||||
|
||||
@@ -4746,7 +4769,7 @@ export function lowerStreamMethodCall(
|
||||
// Transform, and pipe() answers the value itself (the receiver
|
||||
// evaluates first, as in JS).
|
||||
if (isValueStream(lowerer, args[0]!)) {
|
||||
const receiverValue = lowerer.lowerExpr(access.expression);
|
||||
const receiverValue = streamReceiverExpr(lowerer, access.expression);
|
||||
const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
|
||||
const hashValue = lowerer.lowerExpr(args[0]!);
|
||||
const hash = lowerer.declareHiddenLocal("%pipeHash", hashValue.type);
|
||||
@@ -4796,7 +4819,7 @@ export function lowerStreamMethodCall(
|
||||
);
|
||||
}
|
||||
const end = pipeEndOption(lowerer, args[1], loc);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: generalPipe(lowerer, dst.type),
|
||||
@@ -4809,7 +4832,7 @@ export function lowerStreamMethodCall(
|
||||
if (member === "unpipe") {
|
||||
requireSide(readableSide, "unpipe");
|
||||
if (args.length > 1) lowerer.noLowering(`unpipe with ${args.length} arguments`, call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
if (args[0] !== undefined) {
|
||||
const dst = lowerer.lowerExpr(args[0]);
|
||||
if (dst.type.kind === "dyn") return unpipeDyn(lowerer, receiver, dst, loc);
|
||||
@@ -4870,7 +4893,7 @@ export function lowerStreamMethodCall(
|
||||
|
||||
if (member === "destroy") {
|
||||
if (args.length > 1) lowerer.noLowering(`destroy with ${args.length} arguments`, call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
if (args[0] !== undefined) {
|
||||
const err = lowerer.lowerExpr(args[0]);
|
||||
const rootsAtError = (t: IrType): boolean => {
|
||||
@@ -4923,7 +4946,7 @@ export function lowerStreamMethodCall(
|
||||
// the callback took its place (a byte-mode side encodes a string
|
||||
// chunk with it).
|
||||
const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => {
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
|
||||
const tail: IrExpr[] = [];
|
||||
if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN));
|
||||
@@ -4953,7 +4976,7 @@ export function lowerStreamMethodCall(
|
||||
}
|
||||
return writeEnc(args[1], undefined);
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
|
||||
return { kind: "libCall", fn: "writable.writeDyn", args: [receiver, chunk], type: BOOL, loc };
|
||||
}
|
||||
@@ -4963,7 +4986,7 @@ export function lowerStreamMethodCall(
|
||||
// lowered value may have collapsed to one arm (checker narrowing) —
|
||||
// re-dispatch on its IR type.
|
||||
if (args.length > 1) lowerer.noLowering("write with a union chunk and more arguments", call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const chunk = lowerer.lowerExpr(args[0]!);
|
||||
const fn =
|
||||
chunk.type.kind === "union"
|
||||
@@ -4986,7 +5009,7 @@ export function lowerStreamMethodCall(
|
||||
return writeEnc(args[1], undefined);
|
||||
cbArg = args[1];
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const chunk =
|
||||
kind === "bytes"
|
||||
? lowerer.lowerExprExpecting(args[0]!, BYTES)
|
||||
@@ -5021,7 +5044,7 @@ export function lowerStreamMethodCall(
|
||||
encNode = args[1];
|
||||
cbArg = args[2];
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
let flags = 0;
|
||||
const tail: IrExpr[] = [];
|
||||
if (chunkNode !== undefined) {
|
||||
@@ -5068,7 +5091,7 @@ export function lowerStreamMethodCall(
|
||||
if (member === "cork" || member === "uncork") {
|
||||
requireSide(writableSide, member);
|
||||
if (args.length !== 0) lowerer.noLowering(`${member} with arguments`, call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: member === "cork" ? "writable.cork" : "writable.uncork",
|
||||
@@ -5088,7 +5111,7 @@ export function lowerStreamMethodCall(
|
||||
);
|
||||
}
|
||||
const enc = bufEncoding(lowerer, "setEncoding", args[0]!);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "readable.setEncoding",
|
||||
@@ -5104,7 +5127,7 @@ export function lowerStreamMethodCall(
|
||||
requireSide(writableSide, "setDefaultEncoding");
|
||||
if (args.length > 1)
|
||||
lowerer.noLowering(`setDefaultEncoding with ${args.length} arguments`, call);
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const receiver = streamReceiverExpr(lowerer, access.expression);
|
||||
const enc = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc);
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -5257,7 +5280,7 @@ export function lowerStreamProperty(
|
||||
const propCall = (type: IrType): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "stream.prop",
|
||||
args: [lowerer.lowerExpr(expr.expression), strLit(name, loc)],
|
||||
args: [streamReceiverExpr(lowerer, expr.expression), strLit(name, loc)],
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
@@ -5280,7 +5303,7 @@ export function lowerStreamProperty(
|
||||
const read: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "readable.flowing",
|
||||
args: [lowerer.lowerExpr(expr.expression)],
|
||||
args: [streamReceiverExpr(lowerer, expr.expression)],
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
@@ -5294,7 +5317,7 @@ export function lowerStreamProperty(
|
||||
const read: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "stream.errored",
|
||||
args: [lowerer.lowerExpr(expr.expression)],
|
||||
args: [streamReceiverExpr(lowerer, expr.expression)],
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -378,7 +378,7 @@ done:
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v28");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v29");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -712,7 +712,7 @@ done:
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v28");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v29");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7420-for-await-close.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7421-for-await-close.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7422-for-await-untyped-streams.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7423-stream-untyped-bindings.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -340,7 +340,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: RUNTIME_PACK_MATRIX.target,
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -129,6 +129,7 @@ export const OPTIONAL_RUNTIME_SOURCES = [
|
||||
["scr_dyn_handle_props.c", "handleProps"],
|
||||
// for await over checked-dynamic values.
|
||||
["scr_dyn_for_await.c", "dynForAwait"],
|
||||
["scr_dyn_for_await_stream.c", all("dynForAwait", "stream", "dynAsync")],
|
||||
// fs.ReadStream / fs.WriteStream reference only the stream unit and base units.
|
||||
["scr_fs_stream.c", "stream"],
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
|
||||
@@ -437,7 +437,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: matrix.target,
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_runtime_abi_v28(void) {}
|
||||
void scr_runtime_abi_v29(void) {}
|
||||
|
||||
void scr_init(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
|
||||
@@ -81,7 +81,7 @@ void scr_init(void);
|
||||
/* Program objects emitted by the bundled LLVM helper reference this symbol.
|
||||
* Its versioned spelling makes a mismatched manual runtime link fail before
|
||||
* the program can start. */
|
||||
void scr_runtime_abi_v28(void);
|
||||
void scr_runtime_abi_v29(void);
|
||||
void scr_runtime_workers_v8(void);
|
||||
|
||||
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
|
||||
|
||||
@@ -839,6 +839,7 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell);
|
||||
/* for await over a checked-dynamic value (scr_dyn_for_await.c). */
|
||||
ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell);
|
||||
bool scr_dyn_async_iterator_was_sync(void);
|
||||
void scr_dyn_async_iterator_set_sync(bool sync);
|
||||
ScrDyn *scr_dyn_iterator_optional(const ScrDyn *value);
|
||||
/* from.js's probe: 2 when value[Symbol.asyncIterator] is set, 1 when
|
||||
* value[Symbol.iterator] is, 0 for neither, -1 with an error pending. */
|
||||
|
||||
@@ -38,6 +38,11 @@ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) {
|
||||
return iterator;
|
||||
}
|
||||
|
||||
/* For an iterator another open path made (scr_dyn_for_await_stream.c). */
|
||||
void scr_dyn_async_iterator_set_sync(bool sync) {
|
||||
scr_for_await_sync = sync;
|
||||
}
|
||||
|
||||
/* Whether the iterator the last scr_dyn_async_iterator_open returned is a
|
||||
* synchronous one. Read immediately after the open. */
|
||||
bool scr_dyn_async_iterator_was_sync(void) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/* `for await` over a checked-dynamic value in a program that links the
|
||||
* stream unit: a native readable stream behind the value (a boxed stream,
|
||||
* or a Cipheriv/Decipheriv handle's Transform) iterates through an
|
||||
* iterator object whose next() answers the readable iterator's step as
|
||||
* { value, done } and whose return() closes it as a typed loop does
|
||||
* (destroy with an AbortError). Any other value takes
|
||||
* scr_dyn_async_iterator_open. Linked only by programs with such a loop
|
||||
* and streams. */
|
||||
#include "scr_runtime.h"
|
||||
#include "scr_stream.h"
|
||||
|
||||
/* caps[0]: the stream */
|
||||
static ScrStream *iter_stream(ScrClosure *clo) {
|
||||
return scr_box_get_ref(clo->caps[0]); /* +1 */
|
||||
}
|
||||
|
||||
static ScrDyn *iter_next_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
||||
(void)args;
|
||||
(void)argc;
|
||||
ScrStream *s = iter_stream(clo);
|
||||
ScrPromise *p = scr_stream_next_result_dyn(s);
|
||||
scr_stream_release(s);
|
||||
ScrDyn *d = scr_dyn_new_promise(p);
|
||||
scr_promise_release(p);
|
||||
return d;
|
||||
}
|
||||
|
||||
static ScrDyn *iter_return_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
||||
(void)args;
|
||||
(void)argc;
|
||||
ScrStream *s = iter_stream(clo);
|
||||
scr_stream_release(scr_stream_iterator_close(s));
|
||||
scr_stream_release(s);
|
||||
ScrDyn *result = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(result, "value", 5, scr_dyn_retain(scr_dyn_undefined()));
|
||||
scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(true));
|
||||
ScrPromise *p = scr_promise_new();
|
||||
scr_promise_fulfill_ref(p, result, scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
||||
ScrDyn *d = scr_dyn_new_promise(p);
|
||||
scr_promise_release(p);
|
||||
return d;
|
||||
}
|
||||
|
||||
static ScrDyn *iter_method(ScrStream *s, ScrDynThunk thunk, const char *name) {
|
||||
ScrClosure *clo = scr_closure_new((void *)thunk, 1);
|
||||
clo->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace);
|
||||
scr_box_set_ref(clo->caps[0], scr_stream_retain(s));
|
||||
return scr_dyn_new_func(clo, thunk, 0, "()", name);
|
||||
}
|
||||
|
||||
ScrDyn *scr_stream_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) {
|
||||
ScrStream *s = scr_stream_native_readable_of_dyn(value);
|
||||
if (!s) return scr_dyn_async_iterator_open(value, spell);
|
||||
/* the loop awaits its results with the async protocol */
|
||||
scr_dyn_async_iterator_set_sync(false);
|
||||
ScrDyn *it = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(it, "next", 4, iter_method(s, &iter_next_thunk, "next"));
|
||||
scr_dyn_obj_set(it, "return", 6, iter_method(s, &iter_return_thunk, "return"));
|
||||
scr_stream_release(s);
|
||||
return it;
|
||||
}
|
||||
@@ -145,9 +145,12 @@ struct ScrStreamState {
|
||||
bool object_mode_opt;
|
||||
/* The parked for-await next() promise (at most one — the loop awaits
|
||||
* each chunk before asking again). next_dyn picks the resolution
|
||||
* shape: a dyn boxed by tag (the JS lane), or bytes. */
|
||||
* shape: a dyn boxed by tag (the JS lane), or bytes; next_result
|
||||
* wraps the dyn in an iterator result ({ value, done }) for the
|
||||
* checked-dynamic for-await protocol. */
|
||||
ScrPromise *next_waiter;
|
||||
bool next_dyn;
|
||||
bool next_result;
|
||||
/* The iterator's own read() is calling _read: synchronous pushes
|
||||
* buffer, and the waiter takes the whole buffer afterwards (Node's
|
||||
* read() returns everything _read pushed). */
|
||||
@@ -1226,7 +1229,15 @@ static void scr_stream_next_fulfill(ScrStreamState *st, ScrPromise *w /*moves*/,
|
||||
scr_promise_release(w);
|
||||
return;
|
||||
}
|
||||
if (dyn) {
|
||||
if (dyn && st->r.next_result) {
|
||||
/* { value, done }: the value, or undefined at the end */
|
||||
ScrDyn *result = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(result, "value", 5,
|
||||
chunk == NULL ? scr_dyn_retain(scr_dyn_undefined())
|
||||
: scr_stream_entry_box(chunk, kind));
|
||||
scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(chunk == NULL));
|
||||
payload = result;
|
||||
} else if (dyn) {
|
||||
payload = chunk == NULL ? scr_stream_from_done() : scr_stream_entry_box(chunk, kind);
|
||||
} else {
|
||||
payload = chunk ? (ScrBytes *)chunk : scr_bytes_new(SCR_BYTES_U8, 0);
|
||||
@@ -1352,11 +1363,12 @@ static void scr_stream_settle_next(ScrStream *s) {
|
||||
}
|
||||
}
|
||||
|
||||
static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) {
|
||||
static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn, bool result) {
|
||||
ScrStreamState *st = s->st;
|
||||
ScrPromise *p = scr_promise_new();
|
||||
if (!st->has_r) {
|
||||
st->r.next_dyn = dyn;
|
||||
st->r.next_result = result;
|
||||
scr_stream_next_fulfill(st, scr_promise_retain(p), NULL, SCR_ENTRY_BYTES);
|
||||
return p;
|
||||
}
|
||||
@@ -1368,6 +1380,7 @@ static ScrPromise *scr_stream_next_chunk_impl(ScrStream *s, bool dyn) {
|
||||
return p;
|
||||
}
|
||||
st->r.next_dyn = dyn;
|
||||
st->r.next_result = result;
|
||||
st->r.next_waiter = scr_promise_retain(p);
|
||||
st->r.next_parked = false;
|
||||
if (!st->r.next_started) {
|
||||
@@ -1391,11 +1404,15 @@ ScrPromise *scr_stream_next_chunk(ScrStream *s) {
|
||||
scr_promise_reject_pending(p);
|
||||
return p;
|
||||
}
|
||||
return scr_stream_next_chunk_impl(s, false);
|
||||
return scr_stream_next_chunk_impl(s, false, false);
|
||||
}
|
||||
|
||||
ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s) {
|
||||
return scr_stream_next_chunk_impl(s, true);
|
||||
return scr_stream_next_chunk_impl(s, true, false);
|
||||
}
|
||||
|
||||
ScrPromise *scr_stream_next_result_dyn(ScrStream *s) {
|
||||
return scr_stream_next_chunk_impl(s, true, true);
|
||||
}
|
||||
|
||||
/* ── Readable.from (array, string and Buffer sources) ─────────────────── */
|
||||
@@ -7524,6 +7541,18 @@ static ScrStream *scr_stream_boxed(const ScrDyn *value) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The native readable stream behind a checked-dynamic value (+1): a
|
||||
* boxed stream or a Cipheriv/Decipheriv handle's Transform, or NULL. */
|
||||
ScrStream *scr_stream_native_readable_of_dyn(const ScrDyn *value) {
|
||||
ScrStream *s = scr_stream_of_handle(value);
|
||||
if (!s) s = scr_stream_boxed(value);
|
||||
if (s && !s->st->has_r) {
|
||||
scr_stream_release(s);
|
||||
return NULL;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/* stream.handleStream: the stream of a value typed as a stream handle
|
||||
* class (a Cipheriv or Decipheriv), +1. A value of that type can also be
|
||||
* the stream itself, boxed (the stream addAbortSignal answers). */
|
||||
|
||||
@@ -301,6 +301,14 @@ bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb
|
||||
bool scr_stream_write_enc(ScrStream *s, const ScrDyn *chunk, ScrDyn *enc, ScrClosure *cb);
|
||||
ScrPromise *scr_stream_next_chunk(ScrStream *s);
|
||||
ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s);
|
||||
/* The same step resolving with an iterator result ({ value, done }). */
|
||||
ScrPromise *scr_stream_next_result_dyn(ScrStream *s);
|
||||
/* The native readable stream behind a checked-dynamic value (+1), or NULL. */
|
||||
ScrStream *scr_stream_native_readable_of_dyn(const struct ScrDyn *value);
|
||||
/* for await over a checked-dynamic value in a program with streams
|
||||
* (scr_stream_ops.c): a native readable stream iterates as a typed loop
|
||||
* does; any other value takes scr_dyn_async_iterator_open. */
|
||||
struct ScrDyn *scr_stream_dyn_async_iterator_open(struct ScrDyn *value, ScrStr *spell);
|
||||
/* node:stream/promises — the promise forms over the finished/pipeline
|
||||
* machinery above: a pending void promise the terminal watcher settles
|
||||
* (fulfilled on a clean finish, rejected with the finish status
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Leaving a for await loop over an async generator early closes it, as
|
||||
// AsyncIteratorClose does: return, throw and break inside the body, a
|
||||
// labeled continue to an outer loop, and a return() at a yield in the
|
||||
// body all await the generator's return(), so its finally runs before the
|
||||
// loop's caller continues. An exhausted generator and a rejected next()
|
||||
// are not closed. After a throw, an error from closing is dropped; after
|
||||
// a break or return it propagates.
|
||||
const log: string[] = [];
|
||||
|
||||
async function* numbers(tag: string, failClose = false): AsyncGenerator<number> {
|
||||
try {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
} finally {
|
||||
log.push(`${tag} finally`);
|
||||
if (failClose) throw new Error(`${tag} close failed`);
|
||||
}
|
||||
}
|
||||
|
||||
async function* slowClose(tag: string): AsyncGenerator<number> {
|
||||
try {
|
||||
yield 1;
|
||||
} finally {
|
||||
await null;
|
||||
log.push(`${tag} finally after await`);
|
||||
}
|
||||
}
|
||||
|
||||
async function firstOf(tag: string): Promise<number> {
|
||||
for await (const n of numbers(tag)) return n * 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
async function throwsInside(tag: string): Promise<void> {
|
||||
for await (const n of numbers(tag)) throw new Error(`${tag} body ${n}`);
|
||||
}
|
||||
|
||||
async function* takeOne(tag: string): AsyncGenerator<number> {
|
||||
for await (const n of numbers(tag)) {
|
||||
yield n;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function* rethrowing(tag: string): AsyncGenerator<string> {
|
||||
for await (const n of numbers(tag)) {
|
||||
if (n === 2) throw new Error(`${tag} at ${n}`);
|
||||
yield `${tag} ${n}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
log.push(`first ${await firstOf("return")}`);
|
||||
try {
|
||||
await throwsInside("throw");
|
||||
} catch (e) {
|
||||
log.push(`caught ${(e as Error).message}`);
|
||||
}
|
||||
for await (const n of takeOne("generator-return")) log.push(`took ${n}`);
|
||||
try {
|
||||
for await (const s of rethrowing("generator-throw")) log.push(s);
|
||||
} catch (e) {
|
||||
log.push(`caught ${(e as Error).message}`);
|
||||
}
|
||||
const early = takeOne("consumer-return");
|
||||
for await (const n of early) {
|
||||
log.push(`consumer got ${n}`);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
for await (const n of numbers("break-close-fails", true)) {
|
||||
log.push(`break at ${n}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
log.push(`caught ${(e as Error).message}`);
|
||||
}
|
||||
try {
|
||||
for await (const n of numbers("throw-close-fails", true)) throw new Error(`body ${n}`);
|
||||
} catch (e) {
|
||||
log.push(`caught ${(e as Error).message}`);
|
||||
}
|
||||
void Promise.resolve().then(() => log.push("microtask"));
|
||||
for await (const n of slowClose("slow")) {
|
||||
log.push(`slow ${n}`);
|
||||
break;
|
||||
}
|
||||
log.push("after slow");
|
||||
outer: for (const k of [1, 2]) {
|
||||
for await (const n of numbers(`labeled ${k}`)) {
|
||||
log.push(`labeled ${k} got ${n}`);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
for await (const n of numbers("exhausted")) log.push(`exhausted ${n}`);
|
||||
for (const line of log) console.log(line);
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,53 @@
|
||||
// The same early exits in JavaScript: a for await loop that returns,
|
||||
// throws or breaks closes its async generator, including one that reaches
|
||||
// the loop as an untyped parameter, and a source whose return() is still
|
||||
// running finishes before the code after the loop.
|
||||
const log = [];
|
||||
|
||||
async function* letters(tag) {
|
||||
try {
|
||||
yield "a";
|
||||
yield "b";
|
||||
} finally {
|
||||
log.push(`${tag} finally`);
|
||||
}
|
||||
}
|
||||
|
||||
async function firstLetter(source) {
|
||||
for await (const c of source) return c;
|
||||
}
|
||||
|
||||
async function failOn(source, target) {
|
||||
for await (const c of source) {
|
||||
if (c === target) throw new Error(`failed on ${c}`);
|
||||
log.push(`kept ${c}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function* upperUntil(source, stop) {
|
||||
for await (const c of source) {
|
||||
if (c === stop) return;
|
||||
yield c.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
log.push(`first ${await firstLetter(letters("param-return"))}`);
|
||||
try {
|
||||
await failOn(letters("param-throw"), "b");
|
||||
} catch (e) {
|
||||
log.push(`caught ${e.message}`);
|
||||
}
|
||||
for await (const c of upperUntil(letters("stage-return"), "b")) log.push(`upper ${c}`);
|
||||
let ticks = 0;
|
||||
const tick = () => {
|
||||
log.push(`tick ${ticks}`);
|
||||
if (++ticks < 4) process.nextTick(tick);
|
||||
};
|
||||
process.nextTick(tick);
|
||||
for await (const c of letters("break")) {
|
||||
log.push(`break ${c}`);
|
||||
break;
|
||||
}
|
||||
log.push("after break");
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
console.log(log.join("\n"));
|
||||
@@ -0,0 +1,38 @@
|
||||
// for await over a native stream that reaches the loop as an untyped
|
||||
// value iterates it as a typed loop does, in the same tick order as Node:
|
||||
// whole reads, an early return (the stream is destroyed), a break, an
|
||||
// object-mode Transform and a stream error.
|
||||
import { Readable, PassThrough, Transform } from "node:stream";
|
||||
const log = [];
|
||||
async function readAll(s, tag) {
|
||||
for await (const c of s) log.push(`${tag} ${typeof c} ${String(c)}`);
|
||||
log.push(`${tag} done`);
|
||||
}
|
||||
async function first(s, tag) {
|
||||
for await (const c of s) return `${tag} first ${String(c)}`;
|
||||
}
|
||||
async function breaks(s, tag) {
|
||||
for await (const c of s) { log.push(`${tag} got ${String(c)}`); break; }
|
||||
}
|
||||
async function failing(s, tag) {
|
||||
try { for await (const c of s) log.push(`${tag} ${c}`); } catch (e) { log.push(`${tag} caught ${e.message}`); }
|
||||
}
|
||||
let n = 0;
|
||||
const ticker = () => { log.push(`tick ${n}`); if (++n < 6) process.nextTick(ticker); };
|
||||
process.nextTick(ticker);
|
||||
await readAll(Readable.from(["a", "b"]), "arr");
|
||||
const r = Readable.from(["x", "y", "z"]);
|
||||
r.on("close", () => log.push(`r close ${r.destroyed}`));
|
||||
log.push(await first(r, "r"));
|
||||
const pt = new PassThrough();
|
||||
pt.on("close", () => log.push("pt close"));
|
||||
pt.end("bytes");
|
||||
await breaks(pt, "pt");
|
||||
const t = new Transform({ objectMode: true, transform(c, e, cb) { cb(null, { v: c }); } });
|
||||
t.end(1);
|
||||
await (async (s) => { for await (const c of s) log.push(`t ${JSON.stringify(c)}`); })(t);
|
||||
const bad = new Readable({ read() { this.destroy(new Error("boom")); } });
|
||||
await failing(bad, "bad");
|
||||
await readAll(["plain", "array"], "dyn");
|
||||
await new Promise((res) => setTimeout(res, 5));
|
||||
console.log(log.join("\n"));
|
||||
@@ -0,0 +1,33 @@
|
||||
// Streams held where the lowering keeps a checked-dynamic value: a
|
||||
// module-level binding of a stream method's result on a value that a
|
||||
// JavaScript function returns, an element read of an array of streams,
|
||||
// and readable.compose on a stream whose function is typed with JSDoc.
|
||||
import { PassThrough, Readable, Transform } from "node:stream";
|
||||
|
||||
const words = () => Readable.from(["alpha", "beta"]);
|
||||
const shouted = words().map((w) => w.toUpperCase());
|
||||
console.log("shouted", shouted.readable, shouted.readableObjectMode);
|
||||
for await (const w of shouted) console.log("shouted", w);
|
||||
|
||||
const counted = words().filter((w) => w.length > 4);
|
||||
counted.on("data", (w) => console.log("counted", w));
|
||||
await new Promise((resolve) => counted.on("end", resolve));
|
||||
console.log("counted ended", counted.readableEnded);
|
||||
|
||||
const streams = [new PassThrough({ objectMode: true }), new Transform({
|
||||
objectMode: true,
|
||||
transform(chunk, _enc, cb) {
|
||||
cb(null, { wrapped: chunk });
|
||||
},
|
||||
})];
|
||||
streams[1].end(7);
|
||||
for await (const v of streams[1]) console.log("element", JSON.stringify(v));
|
||||
streams[0].end("done");
|
||||
for await (const v of streams[0]) console.log("element", v);
|
||||
|
||||
/** @returns {Readable} */
|
||||
const typedWords = () => Readable.from(["x", "yy"]);
|
||||
const lengths = typedWords().compose(async function* (source) {
|
||||
for await (const w of source) yield `${w}:${w.length}`;
|
||||
});
|
||||
console.log("composed", (await lengths.toArray()).join(","));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2100, "san": 2800 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1900, "san": 2600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1900, "san": 2600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4900, "san": 6454 }
|
||||
@@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
|
||||
runtime_abi: { version: 29, marker: "scr_runtime_abi_v29" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
Reference in New Issue
Block a user