diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 90ce9fddb..d5546f9e2 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -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. diff --git a/docs/content/limitations/language-features/async-iteration.mdx b/docs/content/limitations/language-features/async-iteration.mdx index d36981e2c..ee570f17d 100644 --- a/docs/content/limitations/language-features/async-iteration.mdx +++ b/docs/content/limitations/language-features/async-iteration.mdx @@ -1 +1 @@ -- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. +- **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. diff --git a/docs/content/limitations/language-features/resource-disposal.mdx b/docs/content/limitations/language-features/resource-disposal.mdx index 1f654998b..5163d5322 100644 --- a/docs/content/limitations/language-features/resource-disposal.mdx +++ b/docs/content/limitations/language-features/resource-disposal.mdx @@ -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`). diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 0ecd06292..64f7dbbc2 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "c79533e05530df7a2a01", + "artifactVersion": "e8a11a8f3d91d2503aae", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 0f144e7b3..89837a93a 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -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" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 2ecb82edc..31b68db76 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -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" ] } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 1b7585265..32218613b 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -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" ] }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 1907449b0..022cacb58 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -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"] } diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 17e9ea054..9290f6021 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -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; @@ -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 diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 8c3571c3d..05d3c2784 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -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 }); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7d0d98bb2..74638eff0 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -135,6 +135,7 @@ export const LIB_FN_SYMS: Record = { "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 = { "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", diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index 1c65da36f..ac45129f0 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -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. */ diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 4a32f403b..45814cb8b 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index 8d5b9c9ee..9dafc0319 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index eb33b0ba4..c54aa6884 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ecec14fc5..8c06a28df 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -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 , 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 }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 2521ded2e..3bc04ce07 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -21,28 +21,6 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet = 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" diff --git a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts new file mode 100644 index 000000000..43e8b4959 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts @@ -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, + }, + ]; + }, + }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 1ad34f32f..e0a8c79ba 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 36fcc8c84..ef1832fd1 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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)) { diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index dcb69d67a..7e1524022 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 850af3e65..289ab8fce 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 0fff3c86a..25c70e2f4 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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 { diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 6f54713f1..859cd7305 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -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: diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 2dca21ce9..3a4ffd747 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -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, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 940cfb01d..e3f670a34 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -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 (@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())) ); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 855865f7e..1e4ccfd6e 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -31,6 +31,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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 = new Set([ "error.newOptions", "error.ctorOptions", "error.newAggregate", + "error.newSuppressed", "error.toString", "error.stack", "error.cause", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 044e4d85c..97df41fac 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -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 }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 10d4d1a14..227852e71 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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 / diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 06abe2c0a..a73538ef1 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -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; diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index 56d571cfe..463b9bc62 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -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([ diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 33cc6acb6..87f212aee 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -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", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json deleted file mode 100644 index cbefe8acc..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/corpus/5627-exec-callback.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5629-promisify-stream.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5629-promisify-stream.ts.json new file mode 100644 index 000000000..5cd452498 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5629-promisify-stream.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5629-promisify-stream.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5850-using-disposable-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5850-using-disposable-values.ts.json new file mode 100644 index 000000000..6c6e7d429 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5850-using-disposable-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5850-using-disposable-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5851-using-suppressed-error-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5851-using-suppressed-error-values.ts.json new file mode 100644 index 000000000..a020b22bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5851-using-suppressed-error-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5851-using-suppressed-error-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5852-using-disposable-values-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5852-using-disposable-values-js.js.json new file mode 100644 index 000000000..8c557d27e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5852-using-disposable-values-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5852-using-disposable-values-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5854-array-from-async.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5854-array-from-async.ts.json new file mode 100644 index 000000000..927060a98 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5854-array-from-async.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5854-array-from-async.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5855-array-from-async-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5855-array-from-async-js.js.json new file mode 100644 index 000000000..d32f3625d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5855-array-from-async-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5855-array-from-async-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json deleted file mode 100644 index 8f8a2a4b8..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/fixtures/node-types/child-exec-code-fences.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json deleted file mode 100644 index 73db22aca..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/fixtures/node-types/child-exec.ts" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 3bce01fc7..715b69ab0 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -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); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index 1819b9c53..350995e31 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -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); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index b390ae3aa..2f2435013 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -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); diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index 8af5edcd2..57c5857c9 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -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(); diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index fd75333f3..a3616f303 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -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); diff --git a/packages/runtime/src/scr_exception.c b/packages/runtime/src/scr_exception.c index b20b9fb5d..fb152e078 100644 --- a/packages/runtime/src/scr_exception.c +++ b/packages/runtime/src/scr_exception.c @@ -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) { diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index fcb81246d..c9622ceb3 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -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); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index e0472aa1a..00e974a49 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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) { diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index b3d60d354..a29a28ad2 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -356,3 +356,104 @@ void scr_dyn_dirent_finish(ScrDyn *obj) { break; } } + +/* The well-known symbol `Symbol.` 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.] 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()); +} diff --git a/tests/corpus/5627-exec-callback.ts b/tests/corpus/5627-exec-callback.ts deleted file mode 100644 index eed79e738..000000000 --- a/tests/corpus/5627-exec-callback.ts +++ /dev/null @@ -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: \n". -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"); diff --git a/tests/corpus/5628-promisify-timers.ts b/tests/corpus/5628-promisify-timers.ts index 904ffc593..fd53f3814 100644 --- a/tests/corpus/5628-promisify-timers.ts +++ b/tests/corpus/5628-promisify-timers.ts @@ -20,11 +20,15 @@ async function main(): Promise { 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(); diff --git a/tests/corpus/5629-promisify-stream.ts b/tests/corpus/5629-promisify-stream.ts new file mode 100644 index 000000000..3d8e02dac --- /dev/null +++ b/tests/corpus/5629-promisify-stream.ts @@ -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 { + 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(); diff --git a/tests/corpus/5850-using-disposable-values.ts b/tests/corpus/5850-using-disposable-values.ts new file mode 100644 index 000000000..8724f1f8e --- /dev/null +++ b/tests/corpus/5850-using-disposable-values.ts @@ -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 { + 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); +} diff --git a/tests/corpus/5851-using-suppressed-error-values.ts b/tests/corpus/5851-using-suppressed-error-values.ts new file mode 100644 index 000000000..c46c35558 --- /dev/null +++ b/tests/corpus/5851-using-suppressed-error-values.ts @@ -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); +} diff --git a/tests/corpus/5852-using-disposable-values-js.js b/tests/corpus/5852-using-disposable-values-js.js new file mode 100644 index 000000000..85a5c340f --- /dev/null +++ b/tests/corpus/5852-using-disposable-values-js.js @@ -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(); diff --git a/tests/corpus/5854-array-from-async.ts b/tests/corpus/5854-array-from-async.ts new file mode 100644 index 000000000..a2ab6ab1e --- /dev/null +++ b/tests/corpus/5854-array-from-async.ts @@ -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 { + for (let i = 0; i < n; i++) { + log.push("t" + i); + await null; + } +} +function report(label: string, job: Promise): Promise { + return job.then( + (a) => log.push(label + " " + a.join(",")), + (e) => log.push(label + " caught " + (e as Error).message), + ); +} +async function main(): Promise { + 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((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(); diff --git a/tests/corpus/5855-array-from-async-js.js b/tests/corpus/5855-array-from-async-js.js new file mode 100644 index 000000000..972d3d35f --- /dev/null +++ b/tests/corpus/5855-array-from-async-js.js @@ -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("+"))); diff --git a/tests/fixtures/node-types/child-exec-code-fences.ts b/tests/fixtures/node-types/child-exec-code-fences.ts deleted file mode 100644 index 71f627df4..000000000 --- a/tests/fixtures/node-types/child-exec-code-fences.ts +++ /dev/null @@ -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"); -}); diff --git a/tests/fixtures/node-types/child-exec.ts b/tests/fixtures/node-types/child-exec.ts deleted file mode 100644 index 97e9c546d..000000000 --- a/tests/fixtures/node-types/child-exec.ts +++ /dev/null @@ -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)); - }); -}); diff --git a/tests/fixtures/node-types/promisify-timers.ts b/tests/fixtures/node-types/promisify-timers.ts index 04ff14fd0..da2a8cfea 100644 --- a/tests/fixtures/node-types/promisify-timers.ts +++ b/tests/fixtures/node-types/promisify-timers.ts @@ -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(",")); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index d82875089..5a20739d2 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -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"), { diff --git a/tests/harness/windows-differential.test.ts b/tests/harness/windows-differential.test.ts index dc199d38f..7e6ed205d 100644 --- a/tests/harness/windows-differential.test.ts +++ b/tests/harness/windows-differential.test.ts @@ -149,8 +149,6 @@ const WINDOWS_SKIPS: Record = { "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.