Merge main

This commit is contained in:
Chris Tate
2026-10-17 02:35:31 -05:00
63 changed files with 1111 additions and 306 deletions
@@ -5,7 +5,7 @@ Some builtins can be stored and passed as functions:
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`. Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
@@ -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.
- **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.
@@ -1 +1 @@
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `DisposableStack` constructors are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods and supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops also compile. Caught `SuppressedError` values expose `name` and `message`, but not `error` or `suppressed` payloads.
- **Resource disposal:** Top-level and switch-clause `using` declarations, and `DisposableStack` constructors are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`).
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "c79533e05530df7a2a01",
"artifactVersion": "e8a11a8f3d91d2503aae",
"rowCount": 3880
}
@@ -164,9 +164,9 @@
"apiEntries": 39,
"static": {
"supported": 0,
"partial": 18,
"partial": 17,
"refused": 0,
"not-implemented": 21,
"not-implemented": 22,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -9312,9 +9312,9 @@
"inherited": true
},
"static": {
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
}
},
{
@@ -7,8 +7,8 @@
"tierItemCount": 3472,
"tiers": {
"static": {
"audit-partial": 1061,
"verify-gap": 2284,
"audit-partial": 1060,
"verify-gap": 2285,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -3941,16 +3941,12 @@
"priority": "high",
"tiers": {
"static": {
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:child_process.child_process.exec",
"tests": [
"tests/corpus/5627-exec-callback.ts",
"tests/fixtures/node-types/child-exec.ts",
"tests/harness/project-config.test.ts"
]
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:child_process",
"tests": []
}
}
},
@@ -76140,6 +76136,7 @@
"tests/fixtures/node-types/promisify-fs.ts",
"tests/corpus/5628-promisify-timers.ts",
"tests/fixtures/node-types/promisify-timers.ts",
"tests/corpus/5629-promisify-stream.ts",
"tests/harness/project-config.test.ts"
]
}
@@ -131,9 +131,9 @@
"apiEntries": 39,
"static": {
"supported": 0,
"partial": 18,
"partial": 17,
"refused": 0,
"not-implemented": 21,
"not-implemented": 22,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -11663,10 +11663,7 @@
"tests/corpus/3127-spawn-runtime-options.ts",
"tests/corpus/3130-child-generic-streams.ts",
"tests/corpus/3131-child-rest-listeners.js",
"tests/corpus/3132-child-generic-stream-completion.ts",
"tests/corpus/5627-exec-callback.ts",
"tests/fixtures/node-types/child-exec.ts",
"tests/harness/project-config.test.ts"
"tests/corpus/3132-child-generic-stream-completion.ts"
]
},
"anchorSource": "chapter"
@@ -11735,13 +11732,8 @@
"inherited": true
},
"static": {
"status": "partial",
"evidence": "compiler-feature:child_process.child_process.exec",
"tests": [
"tests/corpus/5627-exec-callback.ts",
"tests/fixtures/node-types/child-exec.ts",
"tests/harness/project-config.test.ts"
]
"status": "not-implemented",
"evidence": "compiler-unmatched:child_process"
},
"anchorSource": "exact"
},
@@ -100574,6 +100566,7 @@
"tests/corpus/5625-promisify-exec.ts",
"tests/corpus/5626-promisify-forms.ts",
"tests/corpus/5628-promisify-timers.ts",
"tests/corpus/5629-promisify-stream.ts",
"tests/corpus/node24-deep-equality-properties.js",
"tests/corpus/node24-deep-equality.cjs",
"tests/corpus/node24-deep-equality.ts",
@@ -102231,6 +102224,7 @@
"tests/fixtures/node-types/promisify-fs.ts",
"tests/corpus/5628-promisify-timers.ts",
"tests/fixtures/node-types/promisify-timers.ts",
"tests/corpus/5629-promisify-stream.ts",
"tests/harness/project-config.test.ts"
]
},
+1 -2
View File
@@ -307,7 +307,6 @@
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.exec(command[, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/5627-exec-callback.ts", "tests/fixtures/node-types/child-exec.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
{ "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] },
{ "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] },
@@ -343,7 +342,7 @@
{ "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] }
+13 -4
View File
@@ -2971,10 +2971,10 @@ declare module "child_process" {
args?: string[] | null,
options?: ExecSyncOptions,
): unknown;
/* The async shell form. The callback forms lower like execFile's, with
* the command line run by /bin/sh -c, and util.promisify recognizes it
* statically, as it does execFile; reached calls without a callback
* fence at their site. */
/* The async shell form — declared surface without full lowering (the
* type-level tolerance story): harness code on paths tests don't reach
* must typecheck; a reached call fences at its site. util.promisify
* recognizes it statically, as it does execFile. */
export function exec(
command: string,
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
@@ -3062,6 +3062,12 @@ declare module "util" {
export type TextDecoder = globalThis.TextDecoder;
export var TextEncoder: typeof globalThis.TextEncoder;
export var TextDecoder: typeof globalThis.TextDecoder;
/* stream.pipeline's (...streams: unknown[]) => unknown is assignable to
* every other target's parameter type, so its overload comes first; no
* other target is assignable to it. */
export function promisify(
fn: typeof import("node:stream").pipeline,
): typeof import("node:stream/promises").pipeline;
export function promisify(
fn: typeof import("node:fs").readFile,
): (path: string, encoding: "utf8" | "utf-8") => Promise<string>;
@@ -3098,6 +3104,9 @@ declare module "util" {
windowsHide?: boolean;
},
) => Promise<{ stdout: string; stderr: string }>;
export function promisify(
fn: typeof import("node:stream").finished,
): typeof import("node:stream/promises").finished;
/* Declared surface without full lowering (the type-level tolerance
* story: guarded/diagnostic-path code must TYPECHECK; a reached use
* without a lowering fences at its site). inspect's options carry the
@@ -18,13 +18,12 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
const argv = host.emitExpr(e.args[1]!);
const cb = host.emitExpr(callbackArg);
host.moveTemp(cb);
const shell = host.emitExpr(e.args[3]!);
const adapter = host.execFileThunkFor(cbT);
host.usesTimers = true;
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`);
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr)`);
const out = B.tmp();
B.line(
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`,
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`,
);
return host.own({ name: out, type: e.type });
}
@@ -135,6 +135,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"error.domCause": "scr_domex_cause",
"error.domClone": "scr_domex_clone",
"error.newAggregate": "scr_aggregate_new",
"error.newSuppressed": "scr_suppressed_new",
"error.aggregateSetErrors": "scr_aggregate_set_errors",
"error.aggregateErrors": "scr_aggregate_errors",
"dyn.errInstanceof": "scr_dyn_err_instanceof",
@@ -1286,6 +1287,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.packPushSpreadIter": "scr_dyn_pack_push_spread_iter",
"dyn.reflectApply": "scr_dyn_reflect_apply",
"dyn.reflectGet": "scr_dyn_reflect_get",
"dyn.usingMethod": "scr_dyn_using_method",
"dyn.usingDispose": "scr_dyn_using_dispose",
"dyn.reflectSet": "scr_dyn_reflect_set",
"dyn.reflectDefine": "scr_dyn_reflect_define",
"dyn.abstractEq": "scr_dyn_abstract_eq",
@@ -628,6 +628,8 @@ const ERROR_KIND_NAMES: readonly string[] = [
"EvalError",
"URIError",
"AggregateError",
"AssertionError",
"SuppressedError",
];
/** A short description of a node that seeds the may-throw fixpoint. */
@@ -205,12 +205,12 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
[
"using-declarations",
"block- and function-scoped using declarations",
"LIFO disposal on normal and abrupt scope exit through [Symbol.dispose]",
"LIFO disposal on normal and abrupt scope exit through [Symbol.dispose], of class instances, object literals and Disposable-typed values; a failing disposal raises a SuppressedError with error and suppressed",
],
[
"await-using-declarations",
"block- and function-scoped await using declarations",
"LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback",
"LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback, of class instances, object literals and AsyncDisposable-typed values",
],
[
"for-using-of",
@@ -56,8 +56,8 @@ export function lowerBuiltinCall(
loc: SrcLoc,
namespaceMember?: ts.MemberName,
): IrExpr {
if (bi.module === "child_process" && (bi.member === "execFile" || bi.member === "exec")) {
return lowerer.lowerExecFileCall(expr, loc, bi.member === "exec");
if (bi.module === "child_process" && bi.member === "execFile") {
return lowerer.lowerExecFileCall(expr, loc);
}
if (bi.module === "events" && bi.member === "addAbortListener") {
return lowerAddAbortListener(lowerer, expr, loc);
@@ -147,12 +147,19 @@ export function registerPromisifiedBuiltinDecl(
module: "timers/promises",
member: target.member,
}
: null;
: target?.module === "stream" &&
(target.member === "finished" || target.member === "pipeline")
? {
kind: "promisified-builtin" as const,
module: "stream/promises",
member: target.member,
}
: null;
if (!projection) {
lowerer.noLowering(
"util.promisify of this target",
argNode ?? e,
"child_process.exec, child_process.execFile, setTimeout, setImmediate and the fs callback functions with an fs/promises counterpart are the supported targets",
"child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline and the fs callback functions with an fs/promises counterpart are the supported targets",
);
}
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
@@ -725,57 +725,35 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
}
/** `execFile(file[, args][, options], callback)` — the asynchronous callback
* slice. With `shell`, `exec(command[, options], callback)`: the command
* line runs with /bin/sh -c, as execSync runs it, and failure messages show
* the command line as Node's do. The child starts immediately with
* stdin/stdout/stderr piped; the runtime captures both outputs and invokes
* the error-first callback after settlement. The callback may ignore a suffix of `(error, stdout,
* slice. The child starts immediately with stdin/stdout/stderr piped; the
* runtime captures both outputs and invokes the error-first callback after
* settlement. The callback may ignore a suffix of `(error, stdout,
* stderr)`, but every declared parameter must have the Node shape. Inline
* `encoding: "utf8"` and a pure numeric `maxBuffer` are accepted; the latter
* is not enforced by the growing native capture, as
* with the synchronous and promisified capture forms. Other options remain
* fenced until their lifecycle can share this asynchronous core. */
export function lowerExecFileCall(
lowerer: Lowerer,
expr: ts.CallExpression,
loc: SrcLoc,
shell = false,
): IrExpr {
const what = shell ? "exec" : "execFile";
export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
if (
expr.arguments.some(ts.isSpreadElement) ||
expr.arguments.length < 2 ||
expr.arguments.length > (shell ? 3 : 4)
expr.arguments.length > 4
) {
lowerer.noLowering(
`${what} with ${expr.arguments.length} arguments`,
`execFile with ${expr.arguments.length} arguments`,
expr,
shell
? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)"
: "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
"the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
);
}
const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined;
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
// exec: /bin/sh -c <command>, execSync's lowering (the runtime reads the
// command line back from argv[1] for failure messages); execFile: the
// file and its args list.
const cmd: IrExpr = shell ? { kind: "strLit", value: "/bin/sh", type: STRING, loc } : first;
const argv: IrExpr = shell
? {
kind: "arrayLit",
elems: [{ kind: "strLit", value: "-c", type: STRING, loc }, first],
type: arrayOf(STRING),
loc,
}
: lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc);
const optionsNode =
expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined;
if (optionsNode) {
const options = optionsNode;
const argv = lowerChildArgsArg(lowerer, argsNode, loc);
if (expr.arguments.length === 4) {
const options = expr.arguments[2]!;
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
`${what} with a non-literal options argument`,
"execFile with a non-literal options argument",
options,
"pass encoding and maxBuffer in an inline object literal",
);
@@ -796,7 +774,7 @@ export function lowerExecFileCall(
const member = optionMember(property);
if (!member)
lowerer.noLowering(
`${what} with this options shape`,
"execFile with this options shape",
property,
"use plain encoding and maxBuffer properties without spreads or computed keys",
);
@@ -806,7 +784,7 @@ export function lowerExecFileCall(
(member.value.text !== "utf8" && member.value.text !== "utf-8")
) {
lowerer.noLowering(
`${what} with a non-literal utf8 encoding`,
"execFile with a non-literal utf8 encoding",
member.value,
"pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)",
);
@@ -814,14 +792,14 @@ export function lowerExecFileCall(
} else if (member.name === "maxBuffer") {
if (!pureNumber(member.value)) {
lowerer.noLowering(
`${what} with a non-literal maxBuffer`,
"execFile with a non-literal maxBuffer",
member.value,
"pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)",
);
}
} else {
lowerer.noLowering(
`${what} option '${member.name}'`,
`execFile option '${member.name}'`,
property,
"only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form",
);
@@ -837,14 +815,14 @@ export function lowerExecFileCall(
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters`,
"execFile callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters",
);
}
if (callback.type.ret.kind !== "void") {
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks returning a value (make the callback body a block, or return nothing)`,
"execFile callbacks returning a value (make the callback body a block, or return nothing)",
);
}
const errorParam = callback.type.params[0];
@@ -859,7 +837,7 @@ export function lowerExecFileCall(
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
`execFile callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
);
}
}
@@ -868,15 +846,9 @@ export function lowerExecFileCall(
lowerer.unsupported(
"SC1090",
callbackNode,
`${what} callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
`execFile callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
);
}
}
return {
kind: "libCall",
fn: "cp.execFile",
args: [cmd, argv, callback, boolLit(shell, loc)],
type: CHILD_T,
loc,
};
return { kind: "libCall", fn: "cp.execFile", args: [cmd, argv, callback], type: CHILD_T, loc };
}
@@ -21,28 +21,6 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
* declaration is not stdlib). Reads only: writes keep their fence (no
* compiled program constructs an errno error). Null for non-error
* receivers and non-stdlib members, so the chain keeps trying. */
/** True for child_process.ExecException, exec's callback error. Its
* `code` is the child's numeric exit status (null after a signal), which
* the runtime error's string code slot does not hold, so reads, `in` tests
* and typeof tests of `code` refuse instead of answering from the slot. */
export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
const sym = t.getSymbol();
return (
sym?.name === "ExecException" &&
lowerer.checker
.declarationsOf(sym)
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))
);
}
export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never {
return lowerer.noLowering(
"ExecException.code",
node,
"spawn reports the exit status through its 'close' event",
);
}
export function lowerErrorCodeProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -104,10 +82,12 @@ export function lowerErrorCodeProperty(
};
}
// %AssertionError's actual, expected, operator, generatedMessage and
// diff are properties of the error's dyn view, read at the declared type.
// diff, and %SuppressedError's error and suppressed, are properties of
// the error's dyn view, read at the declared type.
if (
recvT.className === "%AssertionError" &&
ASSERTION_ERROR_PROPERTIES.has(expr.name.text) &&
((recvT.className === "%AssertionError" && ASSERTION_ERROR_PROPERTIES.has(expr.name.text)) ||
(recvT.className === "%SuppressedError" &&
(expr.name.text === "error" || expr.name.text === "suppressed"))) &&
lowerer.isStdlibMember(expr)
) {
const receiver = lowerer.lowerExpr(expr.expression);
@@ -142,9 +122,6 @@ export function lowerErrorCodeProperty(
!(recvT.className === "%Error" && lowerer.typeOf(expr.expression).isIntersectionType())
)
return null;
if (expr.name.text === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.expression))) {
refuseExecExceptionCode(lowerer, expr);
}
const rawReceiver = lowerer.lowerExpr(expr.expression);
const receiver =
rawReceiver.type.kind === "dyn"
@@ -0,0 +1,338 @@
import { boolLit, numLit, varRef } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import type { Lowerer } from "../lowerer.js";
import {
BOOL,
CAUGHT,
F64,
VOID,
type IrExpr,
type IrParam,
type IrStmt,
type IrType,
type SrcLoc,
typeEquals,
} from "../../../ir/ir.js";
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";
/** `Array.fromAsync(source, mapFn?)` over an array, 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);
* return out;
*
* An array iterates through the async-from-sync iterator: each element is
* 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. */
export function lowerArrayFromAsyncCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null;
if (access.name.text !== "fromAsync") return null;
const loc = locOf(call);
const args = call.arguments;
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(
`Array.fromAsync with ${args.length} arguments`,
call,
"pass the source and an optional mapping function",
);
}
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (resultT?.kind !== "promise" || resultT.inner.kind !== "array")
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",
);
}
lowerer.noLowering(
`Array.fromAsync over a value of type '${lowerer.checker.typeToString(lowerer.typeOf(args[0]!))}'`,
args[0]!,
"an array 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);
if (callback.arity > 2)
lowerer.noLowering("an Array.fromAsync mapping function with three parameters", args[1]);
mapper = callback.fnArg;
const ret = mapper.type.ret;
if (ret.kind !== "promise" && !nonThenable(lowerer, ret)) {
lowerer.noLowering(
"an Array.fromAsync mapping function whose result may be a promise or thenable",
args[1],
"return a value or a Promise",
);
}
}
const name = `%array.fromAsync.${lowerer.liftedFns.length}`;
const context = newFnCtx(false, null, null, outT);
context.isAsync = true;
lowerer.fnStack.push(context);
try {
const src = lowerer.declareHiddenLocal("source", source.type);
const params: IrParam[] = [{ localId: src.id, name: "source", type: source.type }];
let mapperRef: (IrExpr & { type: IrType & { kind: "func" } }) | null = null;
if (mapper) {
const local = lowerer.declareHiddenLocal("mapFn", mapper.type);
params.push({ localId: local.id, name: "mapFn", type: mapper.type });
mapperRef = { kind: "varRef", localId: local.id, type: mapper.type, loc };
}
const out = lowerer.declareHiddenLocal("%fromAsyncOut", outT);
const k = lowerer.declareHiddenLocal("%fromAsyncIndex", F64);
k.mutable = true;
const kRef = varRef(k.id, F64, loc);
const append = (value: IrExpr): IrStmt[] => {
const stmts: IrStmt[] = [];
let item = value;
if (mapperRef) {
const mapped: IrExpr = {
kind: "callValue",
callee: mapperRef,
args: [value, kRef].slice(0, mapperRef.type.params.length),
type: mapperRef.type.ret,
loc,
};
if (mapped.type.kind === "promise") {
item = { kind: "awaitExpr", value: mapped, type: mapped.type.inner, loc };
} else {
// Await of a value that is not a thenable: one resolved promise.
const held = lowerer.declareHiddenLocal("%fromAsyncMapped", mapped.type);
stmts.push({ kind: "varDecl", localId: held.id, init: mapped, loc }, hop(loc));
item = varRef(held.id, held.type, loc);
}
}
stmts.push(
arrayValueStore(
lowerer,
varRef(out.id, outT, loc),
kRef,
// arrayValueStore stores an undefined arm (a hole) as undefined.
typeEquals(item.type, outT.elem) || item.type.kind === "union"
? item
: lowerer.coerceToExpected(item, outT.elem),
outT.elem,
loc,
),
{
kind: "assign",
localId: k.id,
value: { kind: "bin", op: "+", left: kRef, right: numLit(1, loc), type: F64, loc },
loc,
},
);
return stmts;
};
const body: IrStmt[] = [
{
kind: "varDecl",
localId: out.id,
init: { kind: "arrayLit", elems: [], type: outT, loc },
loc,
},
{ kind: "varDecl", localId: k.id, init: numLit(0, loc), loc },
...step.loop(varRef(src.id, source.type, loc), append, loc),
{ kind: "return", value: varRef(out.id, outT, loc), loc },
];
lowerer.liftedFns.push({
name,
params,
returnType: outT,
locals: context.locals,
body,
async: true,
loc,
});
} finally {
lowerer.fnStack.pop();
}
return {
kind: "call",
callee: name,
args: mapper ? [source, mapper] : [source],
type: resultT,
loc,
};
}
function hop(loc: SrcLoc): IrStmt {
return {
kind: "exprStmt",
expr: { kind: "libCall", fn: "async.hop", args: [], type: VOID, loc },
loc,
};
}
/** A value whose await takes one microtask without calling user code:
* primitives, arrays and records without a `then` field. */
function nonThenable(lowerer: Lowerer, type: IrType): boolean {
if (type.kind === "union") {
const arms = lowerer.unions.get(type.unionId)?.arms;
return arms !== undefined && arms.every((arm) => nonThenable(lowerer, arm));
}
if (type.kind === "record") {
const shape = lowerer.shapes.get(type.shapeId);
return shape !== undefined && !shape.indexValue && !shape.fields.some((f) => f.name === "then");
}
return (
type.kind === "f64" ||
type.kind === "bigint" ||
type.kind === "string" ||
type.kind === "bool" ||
type.kind === "symbol" ||
type.kind === "array" ||
type.kind === "undefinedT" ||
type.kind === "nullT"
);
}
/** 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[];
} {
const yieldedT = arrayValueType(lowerer, elem);
let promiseTag: number | null = null;
let valueT = yieldedT;
const promiseElem = elem.kind === "promise" ? elem : null;
if (
promiseElem &&
yieldedT.kind === "union" &&
!["void", "union", "dyn", "undefinedT", "nullT"].includes(promiseElem.inner.kind)
) {
promiseTag = lowerer.armTag(yieldedT.unionId, promiseElem);
valueT = lowerer.withUndefinedArm(promiseElem.inner);
} else if (!nonThenable(lowerer, elem)) {
lowerer.noLowering(
"Array.fromAsync over array values that may be promises or thenables",
node,
"use an array of values or an array of promises",
);
}
return {
valueT,
loop(source, append, loc) {
const arr = lowerer.declareHiddenLocal("%fromAsyncArray", source.type);
const cursor = lowerer.declareHiddenLocal("%fromAsyncCursor", F64);
cursor.mutable = true;
const needsClose = lowerer.declareHiddenLocal("%fromAsyncNeedsClose", BOOL);
needsClose.mutable = true;
const value = lowerer.declareHiddenLocal("%fromAsyncValue", valueT);
value.mutable = true;
const arrRef = varRef(arr.id, source.type, loc);
const cursorRef = varRef(cursor.id, F64, loc);
const read = arrayValueRead(lowerer, arrRef, cursorRef, elem, loc);
const setClose = (on: boolean): IrStmt => ({
kind: "assign",
localId: needsClose.id,
value: boolLit(on, loc),
loc,
});
// A rejected element rejects the next result one reaction later, and
// Node closes the iterator before the returned promise rejects.
const awaitInto = (awaited: IrExpr): IrStmt => {
const error = lowerer.declareHiddenLocal("%fromAsyncNextError", CAUGHT);
return {
kind: "tryCatch",
tryBody: [{ kind: "assign", localId: value.id, value: awaited, loc }],
catchBody: [hop(loc), hop(loc), { kind: "rethrow", localId: error.id, loc }],
catchLocalId: error.id,
finallyBody: null,
loc,
};
};
const next: IrStmt[] = [];
if (promiseTag !== null) {
next.push(
awaitInto({ kind: "awaitUnionExpr", value: read, promiseTag, type: valueT, loc }),
hop(loc),
);
} else {
next.push({ kind: "assign", localId: value.id, value: read, loc }, hop(loc), hop(loc));
}
const length: IrExpr = {
kind: "arrIntrinsic",
method: "length",
receiver: arrRef,
args: [],
type: F64,
loc,
};
const loop: IrStmt = {
kind: "for",
init: { kind: "varDecl", localId: cursor.id, init: numLit(0, loc), loc },
cond: boolLit(true, loc),
update: {
kind: "assign",
localId: cursor.id,
value: { kind: "bin", op: "+", left: cursorRef, right: numLit(1, loc), type: F64, loc },
loc,
},
body: [
setClose(false),
{
kind: "if",
cond: { kind: "bin", op: ">=", left: cursorRef, right: length, type: BOOL, loc },
then: [hop(loc), hop(loc), { kind: "break", loc }],
else_: null,
loc,
},
...next,
setClose(true),
...append(varRef(value.id, valueT, loc)),
],
loc,
};
return [
{ kind: "varDecl", localId: arr.id, init: source, loc },
{ kind: "varDecl", localId: needsClose.id, init: boolLit(false, loc), loc },
{ kind: "varDecl", localId: value.id, init: null, loc },
{
kind: "tryCatch",
tryBody: [loop],
catchBody: null,
catchLocalId: null,
finallyBody: [
{
kind: "if",
cond: varRef(needsClose.id, BOOL, loc),
then: [hop(loc)],
else_: null,
loc,
},
],
loc,
},
];
},
};
}
@@ -68,6 +68,7 @@ import {
lowerArrayFromCall,
lowerArrayOfCall,
} from "./containers/array-construction.js";
import { lowerArrayFromAsyncCall } from "./containers/array-from-async.js";
import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/array-callbacks.js";
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
import { lowerIteratorHelperCall } from "./containers/iterator-helpers.js";
@@ -1392,6 +1393,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerObjectStaticCall(lowerer, expr, expr.expression) ??
lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ??
lowerArrayFromCall(lowerer, expr, expr.expression) ??
lowerArrayFromAsyncCall(lowerer, expr, expr.expression) ??
lowerArrayOfCall(lowerer, expr, expr.expression) ??
lowerer.lowerArrayMethodCall(expr, expr.expression) ??
// Tuple reads preserve the receiver through argument evaluation;
@@ -2027,6 +2027,13 @@ export function collectClassShapeInner(
"extending AssertionError (its constructor takes an options object the subclass constructor would have to forward — extend Error, or construct AssertionError directly)",
);
}
if (lowerer.builtinErrorInfoOf(symbol)?.def.name === "%SuppressedError") {
lowerer.unsupported(
"SC1090",
t,
"extending SuppressedError (extend Error and keep error and suppressed in fields)",
);
}
const named =
(symbol && lowerer.classBySymbol.get(symbol)) ??
lowerer.builtinErrorInfoOf(symbol) ??
@@ -8499,6 +8506,21 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
loc,
};
}
if (errInfo && errInfo.def.name === "%SuppressedError") {
const args = expr.arguments ?? [];
if (args.length > 3 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(`new SuppressedError with this argument list`, expr);
}
return {
kind: "libCall",
fn: "error.newSuppressed",
args: [0, 1, 2].map((index) =>
args[index] ? lowerer.lowerExprExpecting(args[index], DYN) : dynUndefinedExpr(loc),
),
type: { kind: "object", className: "%SuppressedError" },
loc,
};
}
if (errInfo && errInfo.def.name === "%AggregateError") {
const args = expr.arguments ?? [];
if (args.length > 3 || args.some(ts.isSpreadElement)) {
@@ -8,7 +8,6 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
import { lowerReadlineProperty } from "./builtins/readline.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
import { threadsImportOf } from "../threads-import.js";
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
@@ -11119,7 +11118,6 @@ function lowerErrorCodeTypeofTest(
recv = recv.expression;
const errT = errorRootedObjectOf(lowerer, lowerer.typeOf(recv));
if (!errT) continue;
if (isExecExceptionType(lowerer, lowerer.typeOf(recv))) refuseExecExceptionCode(lowerer, prop);
const caught = lowerer.caughtLocalOf(recv);
let receiver: IrExpr;
if (caught) {
@@ -12716,9 +12714,6 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
loc,
};
}
if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) {
refuseExecExceptionCode(lowerer, expr);
}
let recv = lowerer.lowerExpr(expr.right);
const classInfo =
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
@@ -189,7 +189,7 @@ import { fenceNodeModuleMutation } from "./lower-node-module.js";
import { fencePerfEntryKeyWalk } from "./builtins/performance.js";
interface UsingRegistration {
acquire: IrStmt;
acquire: IrStmt[];
cleanup: IrStmt[];
loc: SrcLoc;
}
@@ -403,14 +403,51 @@ function cleanupCall(
return null;
}
/** The statements that dispose `value` at scope exit. `setup` receives
* the statements that run at the declaration, after the value is bound. */
function cleanupForValue(
lowerer: Lowerer,
node: ts.Identifier,
value: IrExpr,
awaited: boolean,
setup: IrStmt[],
): IrStmt[] {
const loc = locOf(node);
if (isUnitType(value.type)) return [];
if (value.type.kind === "dyn") {
// A dyn value (an object literal, a Disposable-typed result): the
// declaration reads and checks its dispose method, and scope exit
// calls it with the value as `this`.
const method = lowerer.declareHiddenLocal("%dispose", DYN);
setup.push({
kind: "varDecl",
localId: method.id,
init: {
kind: "libCall",
fn: "dyn.usingMethod",
args: [value, { kind: "boolLit", value: awaited, type: BOOL, loc }],
type: DYN,
loc,
},
loc,
});
const call: IrExpr = {
kind: "libCall",
fn: "dyn.usingDispose",
args: [{ kind: "varRef", localId: method.id, type: DYN, loc }, value],
type: DYN,
loc,
};
return [
{
kind: "exprStmt",
expr: awaited
? { kind: "libCall", fn: "async.awaitDyn", args: [call], type: DYN, loc }
: call,
loc,
},
];
}
if (value.type.kind === "union") {
const def = lowerer.unions.get(value.type.unionId);
if (!def)
@@ -427,7 +464,9 @@ function cleanupForValue(
type: arm,
loc,
};
const body = cleanupForValue(lowerer, node, narrowed, awaited);
if (arm.kind === "dyn")
lowerer.unsupported("SC1090", node, "disposing a union with an 'unknown' arm");
const body = cleanupForValue(lowerer, node, narrowed, awaited, setup);
out.push({
kind: "if",
cond: {
@@ -513,8 +552,9 @@ function lowerUsingStatement(lowerer: Lowerer, stmt: ts.VariableStatement): Usin
if (!binding)
throw new InternalCompilerError("lowerer bug: using declaration has no runtime binding");
const value = varRef(binding.id, binding.type, locOf(decl.name));
const cleanup = cleanupForValue(lowerer, decl.name, value, awaited);
registrations.push({ acquire, cleanup, loc: locOf(decl) });
const setup: IrStmt[] = [];
const cleanup = cleanupForValue(lowerer, decl.name, value, awaited, setup);
registrations.push({ acquire: [acquire, ...setup], cleanup, loc: locOf(decl) });
}
return registrations;
}
@@ -634,8 +674,8 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
const registrations = lowerUsingStatement(lowerer, stmt);
const accounting: IrStmt[] = [];
for (const registration of registrations) {
out.push(registration.acquire);
accounting.push(registration.acquire, ...registration.cleanup);
out.push(...registration.acquire);
accounting.push(...registration.acquire, ...registration.cleanup);
if (registration.cleanup.length > 0) {
usingMarkers.push({
start: out.length,
@@ -8612,8 +8652,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels);
if (!resourceLoop) return forValues(local.id, body);
const resource = varRef(local.id, local.type, locOf(decl.name));
const cleanup = cleanupForValue(lowerer, decl.name, resource, isAwaitUsing(list));
const setup: IrStmt[] = [];
const cleanup = cleanupForValue(lowerer, decl.name, resource, isAwaitUsing(list), setup);
return forValues(local.id, [
...setup,
{
kind: "tryCatch",
tryBody: body,
@@ -11485,8 +11485,8 @@ export class Lowerer {
return lowerSpawnCall(this, expr, loc);
}
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc, shell = false): IrExpr {
return lowerExecFileCall(this, expr, loc, shell);
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
return lowerExecFileCall(this, expr, loc);
}
lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr {
@@ -2809,7 +2809,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
},
util: {
promisify:
"the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate and the fs callback functions with an fs/promises counterpart; call the bound function directly",
"the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline and the fs callback functions with an fs/promises counterpart; call the bound function directly",
},
"stream/consumers": {
blob:
+5 -4
View File
@@ -138,10 +138,11 @@ const FORCED_OPTIONS: ts.Ts7CompilerOptions = {
// classification is intentionally independent below; this option is only
// the binder's scope decision.
moduleDetection: ts.ModuleDetectionKind.Force,
// Explicit resource management is an ESNext library slice in TS 7 even
// though Node 24 ships the runtime protocol. Include that focused lib
// without widening the rest of the standard-library target past ES2025.
lib: ["lib.es2025.d.ts", "lib.esnext.disposable.d.ts"],
// Explicit resource management and Array.fromAsync are ESNext library
// slices in TS 7 even though Node 24 ships them. Include those focused
// libs without widening the rest of the standard-library target past
// ES2025.
lib: ["lib.es2025.d.ts", "lib.esnext.disposable.d.ts", "lib.esnext.array.d.ts"],
types: [],
allowImportingTsExtensions: true,
allowJs: true,
+5 -20
View File
@@ -1377,23 +1377,6 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return { kind: "object", className: "%Error" };
}
// child_process.ExecException, exec's callback error, is an interface
// extending Error with optional metadata members; the value is one
// ordinary Error as well.
const execException = type.getSymbol();
if (
execException?.name === "ExecException" &&
checker
.declarationsOf(execException)
.some(
(decl) =>
ts.isInterfaceDeclaration(decl) &&
ctx.isStdlibFile(decl.getSourceFile()) &&
isDeclaredInAmbientModule(decl, "child_process"),
)
) {
return { kind: "object", className: "%Error" };
}
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
// still the T it maps.
@@ -2325,10 +2308,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// The stdlib Console interface (the global console and node:console
// Console instances) is a native handle carried by the checked-dynamic
// representation and consumed by the console method lowerings. The
// stdlib Disposable (events.addAbortListener's result) is a checked
// object carrying its Symbol.dispose method.
// stdlib Disposable (events.addAbortListener's result) and
// AsyncDisposable are checked objects carrying their Symbol.dispose or
// Symbol.asyncDispose method.
if (
(psym?.name === "Console" || psym?.name === "Disposable") &&
(psym?.name === "Console" || psym?.name === "Disposable" || psym?.name === "AsyncDisposable") &&
checker
.declarationsOf(psym)
.some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()))
@@ -4900,6 +4884,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
"TypeError",
"URIError",
"AggregateError",
"SuppressedError",
].includes(symbol.name) &&
ctx.checker.declarationsOf(symbol).some((decl) => ctx.isStdlibFile(decl.getSourceFile()))
);
@@ -31,6 +31,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.construct",
"dyn.reflectApply",
"dyn.reflectGet",
"dyn.usingMethod",
"dyn.usingDispose",
"dyn.reflectSet",
"dyn.reflectDefine",
"dyn.abstractEq",
@@ -347,6 +349,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"error.newOptions",
"error.ctorOptions",
"error.newAggregate",
"error.newSuppressed",
"error.toString",
"error.stack",
"error.cause",
+15 -3
View File
@@ -2226,9 +2226,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** child_process.execFile's callback slice: starts an all-piped child,
* captures stdout/stderr, and moves the error-first callback into the
* child registry. The callback shape is program-dependent and checked
* by the validator/backends. The last argument is exec's shell flag:
* the argv is ["-c", command] and failure messages show the command. */
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T },
* by the validator/backends. */
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null], result: CHILD_T },
// spawn's options form: per-slot stdio modes (0 ignore / 1 inherit /
// 2 fd) with the out/err fds for mode 2, detached, env replacement
// pairs, cwd ("" = inherit).
@@ -3030,6 +3029,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
},
"error.aggregateSetErrors": { argTypes: [null, DYN], result: VOID },
"error.aggregateErrors": { argTypes: [null], result: DYN },
/** `new SuppressedError(error, suppressed, message?)` (scr_json.c): all
* three dyn and borrowed; the message conversion can throw. Returns an
* owned %SuppressedError. */
"error.newSuppressed": {
argTypes: [DYN, DYN, DYN],
result: { kind: "object", className: "%SuppressedError" },
},
/** `d instanceof TypeError` (and the other BUILTIN error classes) on a
* checked-dynamic value (scr_json.c): the from_error cache holds the
* dyn↔error identity edge, so the test resolves the runtime error and
@@ -3119,6 +3125,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID },
"dyn.reflectApply": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.reflectGet": { argTypes: [DYN, DYN, DYN], result: DYN },
/** `using` / `await using` with a dyn value (scr_json.c): usingMethod
* runs the declaration's checks and answers what usingDispose calls at
* scope exit (undefined for a null or undefined value). The bool selects
* `await using`. Both throw Node's TypeErrors or the method's error. */
"dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN },
"dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN },
"dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL },
"dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL },
"dyn.abstractEq": { argTypes: [DYN, DYN], result: BOOL },
+4
View File
@@ -1388,6 +1388,10 @@ export const RUNTIME_ERROR_CLASSES: ReadonlyMap<
// constructor. The Error layout; actual, expected, operator,
// generatedMessage and diff are properties of the error's dyn view.
["%AssertionError", { lib: "AssertionError", kind: 9, base: "%Error" }],
// SuppressedError — the error a `using` disposal raises over a pending
// error, and its own constructor. The Error layout; error and suppressed
// are non-enumerable properties of the error's dyn view.
["%SuppressedError", { lib: "SuppressedError", kind: 10, base: "%Error" }],
]);
/** The runtime-provided node:events EventEmitter class (ScrEmitter /
+7 -1
View File
@@ -348,7 +348,13 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
features.tls = true;
}
if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true;
if (fn.startsWith("sym.") || fn === "util.isDeepStrictEqual") features.symbol = true;
if (
fn.startsWith("sym.") ||
fn === "util.isDeepStrictEqual" ||
fn === "dyn.usingMethod" ||
fn === "dyn.usingDispose"
)
features.symbol = true;
if (fn === "process.hrtimeValue") features.bigint = true;
if (fn.startsWith("bigint.")) features.bigint = true;
if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true;
@@ -951,7 +951,6 @@ test("library callbacks retain child, specialized, and generic result diagnostic
{ kind: "strLit", value: "tool", type: STRING, loc },
{ kind: "arrayLit", elems: [], type: arrayOf(STRING), loc },
{ kind: "boolLit", value: true, type: F64, loc },
{ kind: "boolLit", value: false, type: BOOL, loc },
],
};
expect(validateModule(expressionModule(expr, [])).map((error) => error.message)).toEqual([
+3 -3
View File
@@ -2426,7 +2426,7 @@
"name": "util.promisify",
"status": "unsupported",
"code": "SC2020",
"note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate and the fs callback functions with an fs/promises counterpart; call the bound function directly"
"note": "the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline and the fs callback functions with an fs/promises counterpart; call the bound function directly"
},
{
"id": "node-builtin.util.stripVTControlCharacters",
@@ -5446,7 +5446,7 @@
"kind": "syntax",
"name": "block- and function-scoped await using declarations",
"status": "static",
"note": "LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback"
"note": "LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback, of class instances, object literals and AsyncDisposable-typed values"
},
{
"id": "syntax.bigint-literals",
@@ -5621,7 +5621,7 @@
"kind": "syntax",
"name": "block- and function-scoped using declarations",
"status": "static",
"note": "LIFO disposal on normal and abrupt scope exit through [Symbol.dispose]"
"note": "LIFO disposal on normal and abrupt scope exit through [Symbol.dispose], of class instances, object literals and Disposable-typed values; a failing disposal raises a SuppressedError with error and suppressed"
},
{
"id": "syntax.with-statements",
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/corpus/5627-exec-callback.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5629-promisify-stream.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5850-using-disposable-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5851-using-suppressed-error-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5852-using-disposable-values-js.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5854-array-from-async.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5855-array-from-async-js.js"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/fixtures/node-types/child-exec-code-fences.ts"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/fixtures/node-types/child-exec.ts"
],
"diags": []
}
+4 -4
View File
@@ -2265,14 +2265,14 @@ static void scr_exec_file_fire(ScrChild *c) {
}
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
ScrExecFileFn fn, bool shell) {
ScrExecFileFn fn) {
ScrStr *empty = scr_str_new("", 0);
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
NULL, empty);
scr_str_release(empty);
c->exec_cb = cb;
c->exec_fn = fn;
char *display = scr_exec_display(cmd, args, shell);
char *display = scr_exec_display(cmd, args, false);
c->exec_display = scr_str_new(display, strlen(display));
free(display);
scr_child_stream_capture_begin(c->out_stream);
@@ -4905,14 +4905,14 @@ static void scr_exec_file_fire(ScrChild *c) {
}
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
ScrExecFileFn fn, bool shell) {
ScrExecFileFn fn) {
ScrStr *empty = scr_str_new("", 0);
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
NULL, empty);
scr_str_release(empty);
c->exec_cb = cb;
c->exec_fn = fn;
char *display = scr_exec_display(cmd, args, shell);
char *display = scr_exec_display(cmd, args, false);
c->exec_display = scr_str_new(display, strlen(display));
free(display);
scr_child_stream_capture_begin(c->out_stream);
+2 -4
View File
@@ -155,11 +155,9 @@ ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options);
void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */
/* The callback slice: file + args, default options. The returned child has
* all three stdio slots piped; stdout/stderr are captured internally and
* the callback moves into the child registry until settlement. With shell
* (exec), args is ["-c", command] for /bin/sh and failure messages show
* the command, as for scr_exec_sync. */
* the callback moves into the child registry until settlement. */
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/,
ScrExecFileFn fn, bool shell);
ScrExecFileFn fn);
ScrChild *scr_child_retain(ScrChild *c);
void scr_child_release(ScrChild *c);
void *scr_child_retain_v(void *p);
+12
View File
@@ -603,6 +603,18 @@ ScrDyn *scr_dyn_new_symbol(ScrSym *value); /* borrows and retains value */
ScrSym *scr_sym_well_known(ScrStr *name);
ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key);
ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional);
/* `using` and `await using` with a dyn value. scr_dyn_using_method runs the
* declaration's checks: null and undefined answer undefined (nothing to
* dispose); any other value that is not an object or function throws
* Node's TypeError, as does a missing or non-callable [Symbol.dispose].
* With `async`, [Symbol.asyncDispose] is read first and [Symbol.dispose]
* is the fallback, and the answer records which one was found.
* scr_dyn_using_dispose calls that answer with `value` as `this` at scope
* exit and returns the call's result (+1; undefined for a sync fallback,
* whose result `await using` does not await). Both borrow their operands
* and return NULL with the exception pending. */
ScrDyn *scr_dyn_using_method(const ScrDyn *value, bool async);
ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value);
void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored);
void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key);
bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key);
+29 -3
View File
@@ -33,7 +33,8 @@ static const char *const scr_error_names[SCR_ERR_COUNT] = {
/* DOMException's DEFAULT name is "Error" too (WebIDL: the name
* parameter defaults to "Error"); the class identity lives in the
* vtable, never the name. */
"Error", "ReferenceError", "EvalError", "URIError", "AggregateError", "AssertionError"};
"Error", "ReferenceError", "EvalError", "URIError", "AggregateError", "AssertionError",
"SuppressedError"};
void scr_error_trace(void *e, ScrTraceVisit visit, void *ctx) {
/* name/message are strings — never collector-headered; nothing to visit. */
@@ -140,6 +141,28 @@ static void scr_aggregate_destroy(void *obj) {
scr_error_destroy(obj);
}
/* SuppressedError teardown: the ScrError fields plus the disposal
* snapshots scr_exc_suppress recorded for its `error` and `suppressed`. */
static void scr_suppressed_gcfree(void *obj) {
scr_exc_suppressed_forget(obj);
scr_error_gcfree(obj);
}
static void scr_suppressed_destroy(void *obj) {
scr_exc_suppressed_forget(obj);
scr_error_destroy(obj);
}
static void scr_suppressed_reld(void *obj) {
ScrError *e = (ScrError *)obj;
if (--e->rc == 0) {
if (scr_error_traced) scr_cyc_dead(e);
scr_rc_destroy(obj, scr_suppressed_destroy);
} else if (scr_error_traced) {
scr_cyc_released(e);
}
}
static void scr_aggregate_reld(void *obj) {
ScrAggregateError *a = (ScrAggregateError *)obj;
if (--a->base.rc == 0) {
@@ -163,6 +186,7 @@ SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT] = {
{7, 7, &scr_error_reld}, /* URIError */
{8, 8, &scr_aggregate_reld}, /* AggregateError */
{9, 9, &scr_error_reld}, /* AssertionError */
{10, 10, &scr_suppressed_reld}, /* SuppressedError */
};
ScrError *scr_error_retain(ScrError *e) {
@@ -208,11 +232,13 @@ static ScrError *scr_error_alloc(int kind) {
static ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
static ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
static ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR };
static ScrCycType suppressed_type = { &scr_error_trace, &scr_suppressed_gcfree, SCR_CYC_KIND_ERROR };
ScrError *e;
if (scr_error_traced) {
e = scr_cyc_alloc(size, kind == SCR_ERR_DOMEX ? &domex_type
: kind == SCR_ERR_AGGREGATE ? &aggregate_type
: &error_type);
: kind == SCR_ERR_AGGREGATE ? &aggregate_type
: kind == SCR_ERR_SUPPRESSED ? &suppressed_type
: &error_type);
} else {
e = calloc(1, size);
if (!e) scr_error_oom();
+10 -1
View File
@@ -119,7 +119,8 @@ enum {
SCR_ERR_URI = 7,
SCR_ERR_AGGREGATE = 8, /* AggregateError — ScrAggregateError, the wider layout */
SCR_ERR_ASSERTION = 9, /* node:assert's AssertionError — the Error layout */
SCR_ERR_COUNT = 10,
SCR_ERR_SUPPRESSED = 10, /* SuppressedError — the Error layout; error and suppressed are view properties */
SCR_ERR_COUNT = 11,
};
extern SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT]; /* indexed by SCR_ERR_*; main() stamps pre/post */
@@ -201,6 +202,14 @@ ScrError *scr_aggregate_new(const struct ScrDyn *message, const struct ScrDyn *o
void scr_aggregate_set_errors(ScrError *e, struct ScrDyn *errors); /* both borrowed */
struct ScrDyn *scr_aggregate_errors(ScrError *e); /* +1 */
/* new SuppressedError(error, suppressed, message) (scr_json.c): the message
* as in scr_error_new_options, then `error` and `suppressed` as
* non-enumerable properties of the error's dyn view. All arguments are
* borrowed (dyn undefined when absent). Returns +1, or NULL with an
* exception pending when the message conversion throws. */
ScrError *scr_suppressed_new(const struct ScrDyn *error, const struct ScrDyn *suppressed,
const struct ScrDyn *message);
/* Switch runtime-side error allocation to collector-headered (called from
* main() when the compiler's cycle fixpoint marks the hierarchy). */
void scr_error_set_traced(void);
+51 -9
View File
@@ -354,10 +354,41 @@ void scr_rethrow(const ScrCaught *c) {
}
}
/* Explicit resource management's error merge. The full JavaScript object
* exposes `.error` and `.suppressed`; the static catch surface currently
* exposes Error name/message, so retain Node's externally visible identity
* and ordering while consuming both snapshots exactly once. */
/* Explicit resource management's error merge: a SuppressedError whose
* `error` is the disposal error and whose `suppressed` is the body error.
* The snapshots wait in this table for the error's dyn view, so programs
* that never read them link no checked-dynamic code. */
typedef struct ScrSuppressedEntry {
const void *error_object;
ScrCaught *error;
ScrCaught *suppressed;
} ScrSuppressedEntry;
static SCR_TL ScrSuppressedEntry *scr_suppressed_entries = NULL;
static SCR_TL size_t scr_suppressed_len = 0;
static SCR_TL size_t scr_suppressed_cap = 0;
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
const ScrCaught **suppressed) {
for (size_t i = 0; i < scr_suppressed_len; i++) {
if (scr_suppressed_entries[i].error_object != error_object) continue;
*error = scr_suppressed_entries[i].error;
*suppressed = scr_suppressed_entries[i].suppressed;
return true;
}
return false;
}
void scr_exc_suppressed_forget(const void *error_object) {
for (size_t i = 0; i < scr_suppressed_len; i++) {
if (scr_suppressed_entries[i].error_object != error_object) continue;
ScrSuppressedEntry entry = scr_suppressed_entries[i];
scr_suppressed_entries[i] = scr_suppressed_entries[--scr_suppressed_len];
scr_caught_release(entry.error);
scr_caught_release(entry.suppressed);
return;
}
}
void scr_exc_suppress(ScrCaught *suppressed) {
if (suppressed->kind == SCR_EXC_GENRET) {
/* A generator .return() is a return completion, not a thrown error.
@@ -371,11 +402,22 @@ void scr_exc_suppress(ScrCaught *suppressed) {
scr_caught_release(suppressed);
return;
}
scr_caught_release(error);
scr_caught_release(suppressed);
scr_throw_error_named(
scr_str_new("SuppressedError", sizeof("SuppressedError") - 1),
scr_str_new("An error was suppressed during disposal", sizeof("An error was suppressed during disposal") - 1));
static const char text[] = "An error was suppressed during disposal";
ScrStr *message = scr_str_new(text, sizeof text - 1);
ScrError *e = scr_error_new(SCR_ERR_SUPPRESSED, message);
scr_str_release(message);
if (scr_suppressed_len == scr_suppressed_cap) {
size_t cap = scr_suppressed_cap ? scr_suppressed_cap * 2 : 4;
ScrSuppressedEntry *entries = realloc(scr_suppressed_entries, cap * sizeof *entries);
if (!entries) {
fputs("scriptc: out of memory\n", stderr);
abort();
}
scr_suppressed_entries = entries;
scr_suppressed_cap = cap;
}
scr_suppressed_entries[scr_suppressed_len++] = (ScrSuppressedEntry){e, error, suppressed};
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
}
bool scr_caught_instanceof(const ScrCaught *c, size_t pre, size_t post) {
+9 -1
View File
@@ -165,8 +165,16 @@ ScrCaught *scr_caught_retain(ScrCaught *c);
void scr_caught_release(ScrCaught *c); /* NULL-tolerant */
void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx);
/* Replace the pending disposal error with a SuppressedError and consume the
* original body-error snapshot. The current exception cell must be pending. */
* original body-error snapshot. The current exception cell must be pending.
* The SuppressedError keeps both snapshots in a side table until its
* teardown: scr_exc_suppressed_values answers them (borrowed) for the
* error's `error` (the disposal error) and `suppressed` (the body error)
* when scr_json.c builds its dyn view, and scr_exc_suppressed_forget
* releases them. */
void scr_exc_suppress(ScrCaught *suppressed);
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
const ScrCaught **suppressed);
void scr_exc_suppressed_forget(const void *error_object);
/* Borrows the box; re-raises the saved exception exactly (payload RETAINED
* — the binding stays live until its scope exits). */
void scr_rethrow(const ScrCaught *c);
+28
View File
@@ -5677,6 +5677,16 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) {
scr_dyn_obj_set(d, "cause", 5, scr_domex_cause((ScrError *)e));
}
}
/* A disposal SuppressedError's non-enumerable `error` and `suppressed`
* (scr_exc_suppress); a constructed one sets them on its view. */
const ScrCaught *suppressed_error = NULL, *suppressed_value = NULL;
if (e->vt == &scr_error_vts[SCR_ERR_SUPPRESSED] &&
scr_exc_suppressed_values(e, &suppressed_error, &suppressed_value)) {
scr_dyn_obj_set(d, "error", 5, scr_caught_to_dyn(suppressed_error));
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
scr_dyn_obj_set(d, "suppressed", 10, scr_caught_to_dyn(suppressed_value));
d->v.obj.entries[d->v.obj.len - 1].enumerable = false;
}
/* AggregateError's own non-enumerable `errors` list, shared with the
* typed read so both spellings answer the same array. */
if (e->vt == &scr_error_vts[SCR_ERR_AGGREGATE]) {
@@ -10888,6 +10898,24 @@ ScrError *scr_error_new_options(int kind, const ScrDyn *message, const ScrDyn *o
return e;
}
/* SuppressedError's `error` and `suppressed`: non-enumerable own
* properties of the error's dyn view. Both values move in. */
static void scr_suppressed_attach(ScrError *e, ScrDyn *error, ScrDyn *suppressed) {
ScrDyn *view = scr_dyn_from_error(e);
scr_dyn_obj_set(view, "error", 5, error);
view->v.obj.entries[view->v.obj.len - 1].enumerable = false;
scr_dyn_obj_set(view, "suppressed", 10, suppressed);
view->v.obj.entries[view->v.obj.len - 1].enumerable = false;
scr_dyn_release(view);
}
ScrError *scr_suppressed_new(const ScrDyn *error, const ScrDyn *suppressed, const ScrDyn *message) {
ScrError *e = scr_error_new_options(SCR_ERR_SUPPRESSED, message, NULL);
if (!e) return NULL;
scr_suppressed_attach(e, scr_dyn_retain((ScrDyn *)error), scr_dyn_retain((ScrDyn *)suppressed));
return e;
}
ScrDyn *scr_error_cause(ScrError *e) {
size_t hit = scr_errdyn_find(e, true);
if (hit != SIZE_MAX) {
+101
View File
@@ -356,3 +356,104 @@ void scr_dyn_dirent_finish(ScrDyn *obj) {
break;
}
}
/* The well-known symbol `Symbol.<name>` as a dyn key (+1). */
static ScrDyn *scr_using_key(const char *name, size_t len) {
ScrStr *text = scr_str_new(name, len);
ScrSym *symbol = scr_sym_well_known(text);
scr_str_release(text);
ScrDyn *key = scr_dyn_new_symbol(symbol);
scr_sym_release(symbol);
return key;
}
static bool scr_using_typeof_is(const ScrDyn *value, const char *name, size_t len) {
ScrStr *type = scr_dyn_typeof(value);
bool match = type->len == len && !memcmp(type->data, name, len);
scr_str_release(type);
return match;
}
/* The [Symbol.<name>] method of a `using` value: NULL for an absent member
* (undefined or null), the callable member (+1), or NULL with a pending
* TypeError when the member is not callable. */
static ScrDyn *scr_using_get_method(const ScrDyn *value, const char *name, size_t len, bool *absent) {
ScrDyn *key = scr_using_key(name, len);
ScrDyn *method = scr_dyn_symbol_key_get(value, key, false);
scr_dyn_release(key);
*absent = false;
if (!method) return NULL;
if (method->kind == SCR_DYN_UNDEF || method->kind == SCR_DYN_NULL) {
scr_dyn_release(method);
*absent = true;
return NULL;
}
if (!scr_using_typeof_is(method, "function", 8)) {
scr_dyn_release(method);
ScrJsonBuf message;
scr_jb_init(&message);
scr_jb_puts(&message, "Symbol(Symbol.");
scr_jb_write(&message, name, len);
scr_jb_puts(&message, ") is not a function");
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
return NULL;
}
return method;
}
ScrDyn *scr_dyn_using_method(const ScrDyn *value, bool async) {
if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) return scr_dyn_retain(scr_dyn_undefined());
if (!scr_using_typeof_is(value, "object", 6) && !scr_using_typeof_is(value, "function", 8)) {
static const char message[] = "An object is expected with `using` declarations";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return NULL;
}
bool absent = false;
if (async) {
ScrDyn *method = scr_using_get_method(value, "asyncDispose", 12, &absent);
if (!method && !absent) return NULL;
if (method) {
ScrDyn *record = scr_dyn_new_arr();
scr_dyn_arr_push(record, method);
scr_dyn_arr_push(record, scr_dyn_new_bool(false));
return record;
}
}
ScrDyn *method = scr_using_get_method(value, "dispose", 7, &absent);
if (!method) {
if (absent && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
/* A class instance seen through an `unknown` or Disposable-typed
* value: its dyn view carries no methods. */
static const char message[] =
"disposing a class instance through a Disposable- or unknown-typed value has no lowering "
"yet (declare the variable with the class type)";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
} else if (absent) {
static const char message[] = "Symbol(Symbol.dispose) is not a function";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
}
return NULL;
}
if (!async) return method;
ScrDyn *record = scr_dyn_new_arr();
scr_dyn_arr_push(record, method);
scr_dyn_arr_push(record, scr_dyn_new_bool(true));
return record;
}
ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value) {
if (method->kind == SCR_DYN_UNDEF) return scr_dyn_retain(scr_dyn_undefined());
bool sync_fallback = false;
const ScrDyn *callee = method;
if (method->kind == SCR_DYN_ARR) {
callee = method->v.arr.items[0];
sync_fallback = method->v.arr.items[1]->v.b;
}
scr_dyn_this_push_dyn(value);
ScrDyn *result = scr_dyn_call(callee, NULL, 0, "[Symbol.dispose]");
scr_dyn_this_pop();
if (!result || !sync_fallback) return result;
scr_dyn_release(result);
return scr_dyn_retain(scr_dyn_undefined());
}
-36
View File
@@ -1,36 +0,0 @@
// child_process.exec's callback forms: the command line runs with
// /bin/sh -c, the call returns a live ChildProcess immediately, utf8
// stdout/stderr are captured, and the error-first callback fires after the
// current turn. A non-zero exit (a missing command is the shell's 127)
// reports "Command failed: <command>\n<stderr>".
import { exec } from "node:child_process";
import * as childProcess from "node:child_process";
const child = exec("printf out; printf err >&2", (error, stdout, stderr) => {
console.log("ok", error === null, JSON.stringify(stdout), JSON.stringify(stderr));
exec("printf 'a\\nb\\nc\\n' | wc -l", (pipeError, lines) => {
console.log("pipe", pipeError === null, lines.trim());
exec(`printf '%s|' "a b" 'c d' e`, { encoding: "utf8", maxBuffer: 1024 * 1024 }, (_, quoted) => {
console.log("quoting", quoted);
exec("printf partial; printf failed >&2; exit 3", (failed, failedOut, failedErr) => {
console.log("fail", failed !== null, JSON.stringify(failedOut), JSON.stringify(failedErr));
console.log("fail-message", JSON.stringify(failed === null ? "" : failed.message));
exec("exit 1", (silent) => {
console.log("silent", JSON.stringify(silent === null ? "" : silent.message));
exec("scriptc-definitely-not-a-command", (missing, missingOut) => {
const message = missing === null ? "" : missing.message;
console.log("missing", missing !== null, JSON.stringify(missingOut));
console.log("missing-message", message.split("\n")[0]);
exec("true", () => {
console.log("zero-parameter callback");
childProcess.exec("printf namespace", (_, out) => console.log(out));
});
});
});
});
});
});
});
console.log("returned", child.pid !== undefined, child.stdout !== null, child.stderr !== null);
console.log("scheduled");
+6 -2
View File
@@ -20,11 +20,15 @@ async function main(): Promise<void> {
console.log("module timeout:", await sleepFromModule(1, 42));
console.log("module immediate:", await immediateFromModule(true));
// Timers armed together settle in deadline order however late the loop
// reaches its timers phase. (An immediate against a short timer is not
// ordered: the timer fires first once its delay has passed when the loop
// reaches the timers phase, in Node too.)
const order: string[] = [];
const slow = sleep(30, "slow").then((v) => order.push(v));
const fast = sleep(1, "fast").then((v) => order.push(v));
const now = immediate("now").then((v) => order.push(v));
await Promise.all([slow, fast, now]);
const middle = sleep(10, "middle").then((v) => order.push(v));
await Promise.all([slow, fast, middle]);
console.log("order:", order.join(","));
const started = Date.now();
+76
View File
@@ -0,0 +1,76 @@
// util.promisify(stream.finished) and util.promisify(stream.pipeline) are
// Node's custom promisified forms, stream/promises' finished and pipeline:
// fulfilled when the stream (or the last stage) closes cleanly, rejected
// with the stream's error or ERR_STREAM_PREMATURE_CLOSE. The named imports
// and the namespace members are both recognized.
import * as stream from "node:stream";
import { PassThrough, Readable, Transform, Writable, finished } from "node:stream";
import { promisify } from "node:util";
const finishedAsync = promisify(finished);
const pipelineAsync = promisify(stream.pipeline);
function source(items: string[]): Readable {
let i = 0;
return new Readable({
read() {
this.push(i < items.length ? items[i++]! : null);
},
});
}
async function main(): Promise<void> {
const clean = source(["a", "b"]);
const cleanDone = finishedAsync(clean);
clean.resume();
await cleanDone;
console.log("finished: clean");
const torn = new Readable({ read() {} });
const tornDone = finishedAsync(torn);
torn.destroy(new Error("torn"));
try {
await tornDone;
console.log("SHOULD NOT PRINT");
} catch (e) {
if (e instanceof Error) console.log("finished: rejected", e.message);
}
const early = new Readable({ read() {} });
const earlyDone = finishedAsync(early);
early.destroy();
try {
await earlyDone;
console.log("SHOULD NOT PRINT");
} catch (e) {
if (e instanceof Error) console.log("finished: premature", (e as NodeJS.ErrnoException).code);
}
const out: string[] = [];
const sink = new Writable({
write(chunk: Buffer, _enc: string, cb: () => void) {
out.push(chunk.toString());
cb();
},
});
await pipelineAsync(source(["x", "y", "z"]), new PassThrough(), sink);
console.log("pipeline:", out.join(","));
const failing = new Transform({
transform(_chunk: Buffer, _enc: string, cb: (err?: Error | null) => void) {
cb(new Error("boom"));
},
});
const drain = new Writable({
write(_c: Buffer, _e: string, cb: () => void) {
cb();
},
});
try {
await pipelineAsync(source(["q"]), failing, drain);
console.log("SHOULD NOT PRINT");
} catch (e) {
if (e instanceof Error) console.log("pipeline: rejected", e.message, drain.destroyed);
}
}
main();
@@ -0,0 +1,57 @@
// 'using' and 'await using' with Disposable- and AsyncDisposable-typed values:
// object literals and function results, the declaration's TypeErrors, LIFO
// disposal, and the sync fallback of 'await using'. Node.js prints the same.
function attempt(name: string, f: () => void): void {
try {
f();
console.log(name, "ok");
} catch (e) {
console.log(name, (e as Error).name, (e as Error).message);
}
}
const values: unknown[] = [1, "s", true, {}, { [Symbol.dispose]: 5 }, null, undefined];
for (const value of values) {
attempt(String(typeof value), () => {
using x = value as Disposable;
console.log("body");
});
}
function make(label: string): Disposable {
return { [Symbol.dispose]() { console.log("dispose", label); } };
}
attempt("order", () => {
using a = make("a");
using b = make("b");
console.log("body");
});
async function main(): Promise<void> {
const asyncValues: unknown[] = [5, {}, { [Symbol.asyncDispose]: 3 }];
for (const value of asyncValues) {
try {
await using x = value as AsyncDisposable;
console.log("abody");
} catch (e) {
console.log("async", (e as Error).name, (e as Error).message);
}
}
{
await using y = { [Symbol.dispose]() { console.log("sync in await using"); return 7; } } as Disposable;
await using z = { async [Symbol.asyncDispose]() { await null; console.log("async disposed"); } } as AsyncDisposable;
console.log("block");
}
console.log("after");
}
main();
const literal = { [Symbol.dispose]() { console.log("literal disposed"); } };
function scope(): void {
using a = literal;
using b = make("b2");
using c = null;
console.log("scope body");
}
scope();
for (const label of ["p", "q"]) {
using r = make(label);
console.log("loop", label);
}
@@ -0,0 +1,48 @@
// SuppressedError: the error a 'using' disposal raises over a pending error,
// with its error and suppressed values, nested disposals, and the constructor.
// Node.js prints the same.
class Bad implements Disposable {
label: string;
constructor(label: string) {
this.label = label;
}
[Symbol.dispose](): void {
throw new Error("dispose " + this.label);
}
}
try {
using x = new Bad("x");
throw new Error("in body");
} catch (e) {
const se = e as SuppressedError;
console.log(se.name, se.message, e instanceof SuppressedError, e instanceof Error);
console.log((se.error as Error).message, (se.suppressed as Error).message);
console.log(String(se));
}
try {
using a = new Bad("a");
using b = new Bad("b");
throw new RangeError("body");
} catch (e) {
if (e instanceof SuppressedError) {
const inner = e.suppressed;
if (inner instanceof SuppressedError)
console.log((e.error as Error).message, inner.name, (inner.error as Error).message, (inner.suppressed as Error).name);
}
}
try {
using a = new Bad("only");
} catch (e) {
console.log(e instanceof SuppressedError, (e as Error).message);
}
const made = new SuppressedError(1, "two", "msg");
console.log(made.error, made.suppressed, made.message, made.name, made instanceof SuppressedError);
const bare = new SuppressedError(new TypeError("t"), undefined);
console.log(JSON.stringify(bare.message), (bare.error as Error).name, bare.suppressed);
try {
using v = { [Symbol.dispose]() { throw 42; } } as Disposable;
throw "body";
} catch (e) {
const se = e as SuppressedError;
console.log(se.error, se.suppressed);
}
@@ -0,0 +1,29 @@
// 'using' with object literals and function results in JavaScript, and the
// SuppressedError properties. Node.js prints the same.
function res(label, fail) {
return { [Symbol.dispose]() { console.log("close", label); if (fail) throw new Error("dispose " + label); } };
}
{
using a = res("a");
using b = res("b");
console.log("body");
}
try {
using a = res("a", true);
using b = res("b", true);
throw new Error("body");
} catch (e) {
console.log(e.name, e.message, e.error.message, e.suppressed.name, e.suppressed.error.message, e.suppressed.suppressed.message);
console.log(e instanceof SuppressedError, e instanceof Error, Object.keys(e), String(e));
}
const se = new SuppressedError(1, "two", "msg");
console.log(se.error, se.suppressed, se.message, se.name);
async function main() {
{
await using y = { [Symbol.dispose]() { console.log("sync in await using"); } };
await using z = { async [Symbol.asyncDispose]() { await null; console.log("async disposed"); } };
console.log("block");
}
console.log("after");
}
main();
+56
View File
@@ -0,0 +1,56 @@
// Array.fromAsync over arrays of values and of promises, with and without a
// mapping function, interleaved with other promise jobs: every result,
// rejection and iterator close lands on the microtask Node uses. 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;
}
}
function report(label: string, job: Promise<number[]>): Promise<number> {
return job.then(
(a) => log.push(label + " " + a.join(",")),
(e) => log.push(label + " caught " + (e as Error).message),
);
}
async function main(): Promise<void> {
const tick = ticker(24);
const jobs = [
report("values", Array.fromAsync([1, 2, 3])),
report("promises", Array.fromAsync([Promise.resolve(4), Promise.resolve(5)])),
report("empty", Array.fromAsync([] as number[])),
report("map", Array.fromAsync([1, 2], (x: number, i: number) => x * 2 + i)),
report("async map", Array.fromAsync([1, 2], async (x: number) => x + 1)),
report("promise map", Array.fromAsync([3, 4], (x: number) => Promise.resolve(x * x))),
report("promises async map", Array.fromAsync([Promise.resolve(1), Promise.resolve(2)], async (x: number) => x * 5)),
report(
"map throws",
Array.fromAsync([1, 2], (x: number) => {
if (x === 2) throw new Error("map failed");
return x;
}),
),
report(
"async map rejects",
Array.fromAsync([1, 2, 3], async (x: number) => {
if (x === 2) throw new Error("async map failed");
return x;
}),
),
report("first rejected", Array.fromAsync([Promise.reject(new Error("first")), Promise.resolve(1)])),
report("second rejected", Array.fromAsync([Promise.resolve(1), Promise.reject(new Error("second"))])),
report(
"later",
Array.fromAsync([new Promise<number>((resolve) => setTimeout(() => resolve(7), 1)), Promise.resolve(8)]),
),
];
log.push("sync end");
await Promise.all(jobs);
await tick;
console.log(log.join("\n"));
const strings = await Array.fromAsync(["a", "b"], async (s: string, i: number) => s.repeat(i + 1));
console.log(strings.join("|"), strings.length);
}
main();
+14
View File
@@ -0,0 +1,14 @@
// Array.fromAsync in JavaScript over arrays of values and promises, with a
// mapping function, beside other promise jobs. Node.js prints the same.
const order = [];
const job = Array.fromAsync([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)], (x, i) => x * 10 + i);
Promise.resolve()
.then(() => order.push("a"))
.then(() => order.push("b"))
.then(() => order.push("c"))
.then(() => order.push("d"));
job.then((values) => {
order.push("values " + values.join(","));
console.log(order.join(" "));
});
Array.fromAsync(["x", "y"]).then((values) => console.log(values.join("+")));
-8
View File
@@ -1,8 +0,0 @@
import { exec } from "node:child_process";
exec("exit 3", (error) => {
if (error === null) return;
console.log(error.code);
console.log("code" in error);
console.log(typeof error.code === "number");
});
-9
View File
@@ -1,9 +0,0 @@
import { exec } from "node:child_process";
exec("printf typed-exec; printf err >&2", (error, stdout, stderr) => {
if (error !== null) throw error;
console.log(stdout, stderr);
exec("exit 2", { encoding: "utf8" }, (failed) => {
console.log(JSON.stringify(failed === null ? "" : failed.message));
});
});
+1 -1
View File
@@ -11,6 +11,6 @@ console.log(await sleepFromModule(1, true));
const order: string[] = [];
await Promise.all([
sleep(20, "slow").then((v) => order.push(v)),
immediate("now").then((v) => order.push(v)),
sleep(1, "fast").then((v) => order.push(v)),
]);
console.log(order.join(","));
-34
View File
@@ -426,40 +426,6 @@ test("node-types: unsupported child stdin forms remain named fences", async () =
]);
});
test("node-types: callback exec uses the typed ExecException overloads", async () => {
const dir = outDirFor("node-child-exec");
const entry = join(nodeTypesDir, "child-exec.ts");
const result = await compile(entry, {
outPath: join(dir, "child-exec"),
outDir: dir,
sanitize,
});
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const { stdout } = await execFileAsync(result.binaryPath);
expect(stdout).toBe('typed-exec err\n"Command failed: exit 2\\n"\n');
});
test("node-types: exec's numeric ExecException.code remains a named fence", async () => {
const dir = outDirFor("node-child-exec-code-fences");
const entry = join(nodeTypesDir, "child-exec-code-fences.ts");
const result = await compile(entry, {
outPath: join(dir, "child-exec-code-fences"),
outDir: dir,
sanitize,
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
"SC2020",
"SC2020",
"SC2020",
]);
for (const diagnostic of result.diagnostics) {
expect(diagnostic.message).toContain("ExecException.code");
}
});
test("node-types: path and os lower statically under @types/node's shapes", async () => {
const outDir = outDirFor("node-path-os");
const result = await compile(join(nodeTypesDir, "path-os.ts"), {
@@ -149,8 +149,6 @@ const WINDOWS_SKIPS: Record<string, string> = {
"posix-shaped: the uncaught sh ENOENT throw crashes both sides, rendered differently (1645 covers env here)",
"1464-env-writes.ts":
'posix-shaped: the observing child is ENOENT on Windows, exposing the documented spawn-failure ""-vs-null stance',
"5627-exec-callback.ts":
"posix-shaped: exec runs /bin/sh -c (execSync's static shell lowering); Windows Node runs cmd.exe",
// Two programs the events unit's win32 arm surfaced (they compiled for
// the first time once the events gate lifted): both drive their signal
// listeners through a /bin/sh child, ENOENT on Windows Node too.