diff --git a/AGENTS.md b/AGENTS.md index 2350d7337..55ccb2eab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways. +Name a new corpus program after what it tests, without a number: lowercase words joined by hyphens, such as `tests/corpus/intersection-restated-members.ts`, or a directory such as `tests/corpus//main.ts` for a multi-file program. Parallel changes kept taking the same next number; with names, two changes collide only when they add the same name, which git reports as a conflict. Leave existing numbered programs as they are: never rename or renumber them, and name a new program even when it extends a numbered family. + Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/.json`, not in `tests/harness/corpus-costs.json`; see the README there. Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 0fa62e558..efd45af2f 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -14,7 +14,7 @@ A class can extend `require("events")` directly. 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), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), 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.promisify` over an untyped (`any`) function returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`. +`util.promisify` over an untyped function (a value typed `any`, a function whose parameters are untyped, or any other target in JavaScript) returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`, and throws Node's `ERR_INVALID_ARG_TYPE` for values that are not functions. Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise. `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 }`. diff --git a/docs/content/limitations/console-and-process-streams.mdx b/docs/content/limitations/console-and-process-streams.mdx index c62b82f0c..946cb8061 100644 --- a/docs/content/limitations/console-and-process-streams.mdx +++ b/docs/content/limitations/console-and-process-streams.mdx @@ -1,6 +1,13 @@ `console.log`, `info`, and `debug` write to stdout; `error` and `warn` write to stderr. They accept `unknown`, printing strings verbatim and other values through native `util.inspect`. Direct calls support global `console` and `node:console` imports. +Console output, `util.format`, and `util.formatWithOptions` with a format string known only at run time, spread arguments, or an options object format their arguments at run time. +There, `%s` of a function or of a class instance whose class defines `toString` and `%o` of an object throw `SC2020`. +`util.inspect` with `colors`, with option values known only at run time, or in the `inspect(value, showHidden, depth, colors)` form renders the value's untyped view in the runtime with Node's styles. +Colored output of errors, promises, regular expressions, `DataView`, `ArrayBuffer`, weak collections, iterators, and other native objects throws `SC2020`. +Inspect options other than `depth`, `colors`, `customInspect`, `compact` (a number or `false`), `breakLength`, and `sorted` (a boolean) throw `SC2020` when they are set to a value other than Node's default. +`util.inspect` calls `[util.inspect.custom]` methods of classes and objects with Node's depth, options, and `util.inspect` arguments, and `console.dir` and `customInspect: false` skip them. +A class instance with such a method that is held as an untyped value throws `SC2020` when it is inspected, and a class whose subclasses declare the method without declaring it itself is refused. Stored global console values and detached output methods retain identity and accept ordinary arguments. Stored methods do not support format specifiers with substitution arguments. Other stored console APIs, custom `Console` instances, and console mutation are unsupported. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 130da748c..2f90447ad 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -21382,9 +21382,9 @@ "anchor": "err_invalid_mime_syntax", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -23520,9 +23520,9 @@ "anchor": "err_throttled", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -71946,9 +71946,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -71969,9 +71969,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -71992,9 +71992,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72015,9 +72015,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72038,9 +72038,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72061,9 +72061,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72176,9 +72176,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72199,9 +72199,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72222,9 +72222,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72245,9 +72245,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72268,9 +72268,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72291,9 +72291,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72314,9 +72314,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72337,9 +72337,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72360,9 +72360,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -72636,9 +72636,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -72751,9 +72751,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -72774,9 +72774,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -72797,9 +72797,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -73027,9 +73027,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -73050,9 +73050,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -73395,9 +73395,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c3a0f90b3..8060af796 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -25806,8 +25806,12 @@ "anchor": "dep0044-utilisarray", "nodeStability": null, "static": { - "status": "not-applicable", - "evidence": "compiler-chapter-policy:deprecations" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.isArray", + "tests": [ + "tests/corpus/7700-util-deprecated-helpers.cjs", + "tests/corpus/7701-util-deprecated-helpers.ts" + ] }, "anchorSource": "exact" }, @@ -26094,8 +26098,12 @@ "anchor": "dep0060-util_extend", "nodeStability": null, "static": { - "status": "not-applicable", - "evidence": "compiler-chapter-policy:deprecations" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util._extend", + "tests": [ + "tests/corpus/7700-util-deprecated-helpers.cjs", + "tests/corpus/7701-util-deprecated-helpers.ts" + ] }, "anchorSource": "exact" }, @@ -32763,6 +32771,8 @@ "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6708-dh-errors.js", "tests/corpus/7440-tls-server-add-context.ts", + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7711-util-throttle.cjs", "tests/corpus/7714-zlib-brotli-compress.cjs", "tests/corpus/caught-system-error-fields.ts", "tests/corpus/error-stack-frames.ts", @@ -36688,8 +36698,11 @@ "anchor": "err_invalid_mime_syntax", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.ERR_INVALID_MIME_SYNTAX", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs" + ] }, "anchorSource": "exact" }, @@ -38826,8 +38839,11 @@ "anchor": "err_throttled", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:errors" + "status": "partial", + "evidence": "compiler-feature:errors.ERR_THROTTLED", + "tests": [ + "tests/corpus/7711-util-throttle.cjs" + ] }, "anchorSource": "exact" }, @@ -101568,6 +101584,25 @@ "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/6428-promisify-callback-functions.ts", + "tests/corpus/7520-util-format-runtime.ts", + "tests/corpus/7521-util-format-runtime.cjs", + "tests/corpus/7522-promisify-untyped.cjs", + "tests/corpus/7523-promisify-untyped.ts", + "tests/corpus/7524-inspect-custom.ts", + "tests/corpus/7525-inspect-custom.cjs", + "tests/corpus/7526-inspect-options-runtime.ts", + "tests/corpus/7700-util-deprecated-helpers.cjs", + "tests/corpus/7701-util-deprecated-helpers.ts", + "tests/corpus/7702-util-signal-exit-code.cjs", + "tests/corpus/7703-util-aborted.cjs", + "tests/corpus/7704-util-aborted.ts", + "tests/corpus/7705-util-diff.cjs", + "tests/corpus/7706-util-diff.ts", + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts", + "tests/corpus/7710-util-debounce.cjs", + "tests/corpus/7711-util-throttle.cjs", + "tests/corpus/7712-util-debounce-throttle.ts", "tests/corpus/7747-text-codec-untyped-slot.js", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", @@ -101654,8 +101689,12 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util._extend", + "tests": [ + "tests/corpus/7700-util-deprecated-helpers.cjs", + "tests/corpus/7701-util-deprecated-helpers.ts" + ] }, "anchorSource": "exact" }, @@ -101677,8 +101716,12 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.isArray", + "tests": [ + "tests/corpus/7700-util-deprecated-helpers.cjs", + "tests/corpus/7701-util-deprecated-helpers.ts" + ] }, "anchorSource": "exact" }, @@ -101700,8 +101743,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.MIMEType", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101723,8 +101770,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mime.toString", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101746,8 +101797,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mime.toJSON", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101769,8 +101824,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.MIMEType.parse", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101884,8 +101943,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.MIMEParams", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101907,8 +101970,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams.delete", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101930,8 +101997,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams.entries", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101953,8 +102024,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams.get", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101976,8 +102051,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams.has", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -101999,8 +102078,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams.keys", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -102022,8 +102105,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams.set", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -102045,8 +102132,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams.values", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -102068,8 +102159,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.mimeParams", + "tests": [ + "tests/corpus/7707-util-mime-type.cjs", + "tests/corpus/7708-util-mime-type.ts" + ] }, "anchorSource": "exact" }, @@ -102468,8 +102563,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.convertProcessSignalToExitCode", + "tests": [ + "tests/corpus/7702-util-signal-exit-code.cjs" + ] }, "anchorSource": "exact" }, @@ -102603,8 +102701,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.debounce", + "tests": [ + "tests/corpus/7710-util-debounce.cjs", + "tests/corpus/7712-util-debounce-throttle.ts" + ] }, "anchorSource": "exact" }, @@ -102626,8 +102728,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.throttle", + "tests": [ + "tests/corpus/7711-util-throttle.cjs", + "tests/corpus/7712-util-debounce-throttle.ts" + ] }, "anchorSource": "exact" }, @@ -102649,8 +102755,12 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.diff", + "tests": [ + "tests/corpus/7705-util-diff.cjs", + "tests/corpus/7706-util-diff.ts" + ] }, "anchorSource": "exact" }, @@ -102677,7 +102787,9 @@ "tests": [ "tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/5835-util-format-integer-specifiers.ts" + "tests/corpus/5835-util-format-integer-specifiers.ts", + "tests/corpus/7520-util-format-runtime.ts", + "tests/corpus/7521-util-format-runtime.cjs" ] }, "anchorSource": "exact" @@ -102705,7 +102817,9 @@ "tests": [ "tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", - "tests/corpus/5835-util-format-integer-specifiers.ts" + "tests/corpus/5835-util-format-integer-specifiers.ts", + "tests/corpus/7520-util-format-runtime.ts", + "tests/corpus/7521-util-format-runtime.cjs" ] }, "anchorSource": "exact" @@ -102896,7 +103010,8 @@ "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", - "tests/corpus/5884-inspect-layout-options.ts" + "tests/corpus/5884-inspect-layout-options.ts", + "tests/corpus/7526-inspect-options-runtime.ts" ] }, "anchorSource": "exact" @@ -102919,8 +103034,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.util.inspect", + "tests": [ + "tests/corpus/7526-inspect-options-runtime.ts" + ] }, "anchorSource": "exact" }, @@ -102942,8 +103060,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.custom", + "tests": [ + "tests/corpus/7524-inspect-custom.ts", + "tests/corpus/7525-inspect-custom.cjs" + ] }, "anchorSource": "ancestor" }, @@ -103260,6 +103382,8 @@ "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", + "tests/corpus/7522-promisify-untyped.cjs", + "tests/corpus/7523-promisify-untyped.ts", "tests/harness/project-config.test.ts" ] }, @@ -103475,8 +103599,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.aborted", + "tests": [ + "tests/corpus/7703-util-aborted.cjs", + "tests/corpus/7704-util-aborted.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index ab626fe21..5eb4c1b11 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -187,6 +187,13 @@ "node-builtin.util.callbackify": ["tests/corpus/5190-util-callbackify.cjs", "tests/corpus/5191-util-callbackify.ts", "tests/fixtures/node-types/util-callables.ts"], "node-builtin.util.debuglog": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts", "tests/fixtures/node-types/util-callables.ts"], "node-builtin.util.debug": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts"], + "node-builtin.util.isArray": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"], + "node-builtin.util._extend": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"], + "node-builtin.util.convertProcessSignalToExitCode": ["tests/corpus/7702-util-signal-exit-code.cjs"], + "node-builtin.util.aborted": ["tests/corpus/7703-util-aborted.cjs", "tests/corpus/7704-util-aborted.ts"], + "node-builtin.util.diff": ["tests/corpus/7705-util-diff.cjs", "tests/corpus/7706-util-diff.ts"], + "node-builtin.util.debounce": ["tests/corpus/7710-util-debounce.cjs", "tests/corpus/7712-util-debounce-throttle.ts"], + "node-builtin.util.throttle": ["tests/corpus/7711-util-throttle.cjs", "tests/corpus/7712-util-debounce-throttle.ts"], "node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"], "node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"], "node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"], @@ -253,9 +260,11 @@ {"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]}, {"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]}, {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, - {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]}, + {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/7520-util-format-runtime.ts", "tests/corpus/7521-util-format-runtime.cjs"]}, {"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]}, - { "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] }, + { "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/7526-inspect-options-runtime.ts"] }, + { "chapter": "util", "signatures": ["`util.inspect(object[, showHidden[, depth[, colors]]])`"], "status": "partial", "evidence": ["tests/corpus/7526-inspect-options-runtime.ts"] }, + { "chapter": "util", "signatures": ["`custom` Type: {symbol} that can be used to declare custom inspect functions."], "status": "partial", "evidence": ["tests/corpus/7524-inspect-custom.ts", "tests/corpus/7525-inspect-custom.cjs"] }, { "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] }, { "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] }, { "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" }, @@ -265,6 +274,10 @@ { "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/corpus/node24-buffer-encoding.ts", "tests/corpus/node24-buffer-encoding-errors.ts", "tests/fixtures/node-types/text-encoding.ts"] }, { "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-text-encode-into.ts", "tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] }, { "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] }, + { "chapter": "util", "symbols": ["util.MIMEType", "util.MIMEParams", "MIMEType.parse", "mime.toString", "mime.toJSON", "mimeParams.delete", "mimeParams.entries", "mimeParams.get", "mimeParams.has", "mimeParams.keys", "mimeParams.set", "mimeParams.values"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] }, + { "chapter": "util", "signatures": ["`mimeParams[Symbol.iterator]()`"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] }, + { "chapter": "errors", "symbols": ["ERR_INVALID_MIME_SYNTAX"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs"] }, + { "chapter": "errors", "symbols": ["ERR_THROTTLED"], "status": "partial", "evidence": ["tests/corpus/7711-util-throttle.cjs"] }, { "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] }, { "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] }, @@ -409,7 +422,7 @@ { "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": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.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/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.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/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/corpus/7522-promisify-untyped.cjs", "tests/corpus/7523-promisify-untyped.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": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] }, diff --git a/packages/compiler/ambient/node-fallback/util.d.ts b/packages/compiler/ambient/node-fallback/util.d.ts index 5ff4c38ad..d75829827 100644 --- a/packages/compiler/ambient/node-fallback/util.d.ts +++ b/packages/compiler/ambient/node-fallback/util.d.ts @@ -28,6 +28,85 @@ declare module "util" { ): string; export function parseEnv(content: string): { [key: string]: string | undefined }; export function inherits(constructor: unknown, superConstructor: unknown): void; + export class MIMEType { + constructor(input: string | { toString: () => string }); + static parse(input: string | { toString: () => string }): MIMEType | null; + type: string; + subtype: string; + readonly essence: string; + readonly params: MIMEParams; + toString(): string; + toJSON(): string; + } + export class MIMEParams { + delete(name: string): void; + entries(): IterableIterator<[name: string, value: string]>; + get(name: string): string | null; + has(name: string): boolean; + keys(): IterableIterator; + set(name: string, value: string): void; + values(): IterableIterator; + toString(): string; + toJSON(): string; + [Symbol.iterator](): IterableIterator<[name: string, value: string]>; + } + export type DiffEntry = [operation: -1 | 0 | 1, value: string]; + export function diff( + actual: string | readonly string[], + expected: string | readonly string[], + ): DiffEntry[]; + export interface DebounceOptions { + leading?: boolean; + rejectOnCancel?: boolean; + signal?: AbortSignal; + } + /** Every call answers a promise of the result of the call it waits for. */ + export interface DebouncedFunction any> { + (...args: Parameters): Promise>>; + cancel(reason?: unknown): void; + flush(): void; + ref(): this; + unref(): this; + readonly pending: Promise>> | null; + readonly pendingCount: number; + } + export function debounce any>( + fn: T, + wait: number, + options?: DebounceOptions, + ): DebouncedFunction; + export interface ThrottleOptions { + concurrency?: number; + maxPending?: number; + overflow?: "queue" | "drop"; + signal?: AbortSignal; + strict?: boolean; + } + export interface ThrottledFunction any> { + (...args: Parameters): Promise>>; + cancel(reason?: unknown): void; + hasImmediateCapacity(): boolean; + ref(): this; + unref(): this; + readonly pending: Promise>> | null; + readonly pendingCount: number; + readonly activeCount: number; + } + export function throttle any>( + fn: T, + limit: number, + interval: number, + options?: ThrottleOptions, + ): ThrottledFunction; + /** Resolves with the abort event once `signal` aborts, or with undefined + * when it already has. */ + export function aborted(signal: AbortSignal, resource: any): Promise; + /** 128 plus the number of the signal, the exit code of a process it terminates. */ + export function convertProcessSignalToExitCode(signalCode: string): number; + /** @deprecated Since v4.0.0 - Use `Array.isArray()` instead. */ + export function isArray(object: unknown): object is unknown[]; + /** @deprecated Since v6.0.0 - Use `Object.assign()` instead. Answers target itself. */ + export function _extend(target: T, source: object | null | undefined): T; export interface DeprecateOptions { modifyPrototype?: boolean; } @@ -85,7 +164,13 @@ declare module "util" { export type TextDecoder = globalThis.TextDecoder; export var TextEncoder: typeof globalThis.TextEncoder; export var TextDecoder: typeof globalThis.TextDecoder; - /* stream.pipeline's overload comes first: its __promisify__ member keeps + /* An untyped function: an `any` value, or a function whose parameters + * are untyped (JavaScript's `(value, callback) => ...`), the only + * parameters that accept this object type. The promisified function is + * untyped too. It comes first because such functions match every other + * overload. */ + export function promisify(fn: (...args: { readonly "scriptc:untyped": true }[]) => unknown): any; + /* stream.pipeline's overload comes next: its __promisify__ member keeps * every other target out of it. */ export function promisify( fn: typeof import("node:stream").pipeline, @@ -206,15 +291,56 @@ declare module "util" { ): (address: string, port: number) => Promise<{ hostname: string; service: string }>; /* 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 - * documented knobs through an index signature. */ + * without a lowering fences at its site). */ + export interface InspectOptions { + showHidden?: boolean | undefined; + depth?: number | null | undefined; + colors?: boolean | undefined; + customInspect?: boolean | undefined; + showProxy?: boolean | undefined; + maxArrayLength?: number | null | undefined; + maxStringLength?: number | null | undefined; + breakLength?: number | undefined; + compact?: boolean | number | undefined; + sorted?: boolean | ((a: string, b: string) => number) | undefined; + getters?: "get" | "set" | boolean | undefined; + numericSeparator?: boolean | undefined; + } + export type Style = + | "special" + | "number" + | "bigint" + | "boolean" + | "undefined" + | "null" + | "string" + | "symbol" + | "date" + | "regexp" + | "module"; + export interface InspectOptionsStylized extends InspectOptions { + stylize(text: string, styleType: Style): string; + } + export type CustomInspectFunction = ( + depth: number, + options: InspectOptionsStylized, + inspect: typeof import("node:util").inspect, + ) => any; export function inspect( object: unknown, - options?: { depth?: number | null; colors?: boolean; [k: string]: unknown }, + showHidden?: boolean, + depth?: number | null, + color?: boolean, ): string; + export function inspect(object: unknown, options?: InspectOptions): string; + export namespace inspect { + /* The key of an object's custom inspect function: + * Symbol.for('nodejs.util.inspect.custom'). */ + const custom: unique symbol; + } export function format(format?: unknown, ...args: unknown[]): string; export function formatWithOptions( - inspectOptions: { depth?: number | null; colors?: boolean; [k: string]: unknown }, + inspectOptions: InspectOptions, format?: unknown, ...args: unknown[] ): string; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 2a6c6959e..b31317cb5 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -33,6 +33,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.mime ? { mime: true } : {}), + ...(features.utilTiming ? { utilTiming: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), ...(features.builtinValues ? { builtinValues: true } : {}), events: features.processEvents, diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index a43cf3534..cc0af8ebb 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -50,6 +50,7 @@ import { matchIntegerBytesForLoop, matchIntegerCountedForLoop, matchIntegerPairedForLoop, + steppedLoopCounter, } from "../../ir/integer-loops.js"; import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js"; import { @@ -1218,7 +1219,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { // has no exit besides its own bound, which LLVM can vectorize. A typed // array's length is fixed, so each block can take it as its limit when // the loop does not assign the array's binding. + // + // Inside another loop of this function, which polls at least once + // every WORKER_POLL_BLOCK of its own iterations, a loop needs less: + // - a loop whose counters' ranges bound its runs, and those of the + // loops in its body, to WORKER_POLL_BLOCK^2 needs no poll; + // - an innermost counted loop is strip-mined, typed arrays or not, + // and does not poll after its last block. A run of at most + // WORKER_POLL_BLOCK iterations then never polls, and the countdown + // leaves its body. A run of at most WORKER_UNPOLLED_RUN iterations + // is one block, polled once as it leaves. + const enclosed = + emitter.mod.workers === true && + emitter.jumpTargets.some((target) => target.kind === "loop"); + const bounded = + enclosed && boundedLoopRuns(emitter, s) <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK; + const innermost = enclosed && !containsLoop(s.body); const stripMined = + !bounded && capturedLets.length === 0 && integerSlot !== null && ((countedLoop !== null && @@ -1226,16 +1244,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) || (integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) && emitter.workerLoopPolls(s) && - loopAccessesTypedArrays(s); - const workerBudget = stripMined ? null : emitter.workerLoopBudget(s); + (loopAccessesTypedArrays(s) || (countedLoop !== null && innermost)); + const workerBudget = stripMined || bounded ? null : emitter.workerLoopBudget(s); // One slot for every copy of the loop: a speculative copy resumes an // iteration in another copy's body, which then reads this bound. let blockBound: string | null = null; + let runSlot: string | null = null; if (stripMined) { blockBound = B.slot(); B.entryAllocas.push( `${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`, ); + if (innermost) { + runSlot = B.slot(); + B.entryAllocas.push(`${runSlot} = alloca i1 ; cancellation poll after the run`); + } } freshenBindings(); emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => { @@ -1271,7 +1294,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { return len; }, }; - emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit); + emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit, runSlot); return; } const lc = B.newLabel("loop.c"); @@ -2628,6 +2651,60 @@ function emitInt32KindLoop( /** Iterations between cancellation polls of a strip-mined counted loop. */ const WORKER_POLL_BLOCK = 64; +/** The longest run of a strip-mined loop inside another loop that polls + * only as it leaves. */ +const WORKER_UNPOLLED_RUN = 16384; + +/** Whether a step of `step` from every value in `range` gives a different + * value. Beyond 2^53 a double can stop changing (`j + 1 === j` from 2^53), + * so a counter there can run without end inside a range of a few values. */ +function exactSteps(range: { min: number; max: number }, step: number): boolean { + const size = Math.abs(step); + return range.min >= size - 2 ** 53 && range.max <= 2 ** 53 - size; +} + +/** At most how many times a `for` loop and the loops in its body run their + * bodies per entry, from the counters' proven ranges: its iterations times + * one plus the nested loops' runs. Infinity when a count has no bound, or + * depends on a run-time guard (the guard's slow copy steps a double). */ +function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number { + if (loop.kind !== "for") return Infinity; + const counted = matchIntegerCountedForLoop( + loop, + emitter.numericLocals, + emitter.integerRanges, + LOOPS.of(emitter).unaliasedArrays, + ); + let trips = Infinity; + if (counted && !counted.guarded && exactSteps(counted.range, counted.step)) + trips = Math.floor((counted.range.max - counted.range.min) / Math.abs(counted.step)) + 1; + else if (!counted) { + // Each iteration closes the gap between the two counters by at least two. + const paired = matchIntegerPairedForLoop(loop, emitter.numericLocals, emitter.integerRanges); + if (paired && !paired.guarded && exactSteps(paired.range, 1)) + trips = Math.floor((paired.range.max - paired.range.min) / 2) + 1; + } + if (trips === Infinity) { + // A counter with a proven range where the condition reads it, such as + // one compared with a module constant. + const stepped = steppedLoopCounter(loop, emitter.currentLocals); + const range = stepped && emitter.integerRanges.get(stepped.ref); + if (stepped && range && exactSteps(range, stepped.step)) + trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1; + } + if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity; + let nested = 0; + const visit = (s: IrStmt): boolean => { + if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") { + nested += boundedLoopRuns(emitter, s); + return nested !== Infinity; + } + return everyStmtChild(s, () => true, visit); + }; + if (!loop.body.every(visit)) return Infinity; + return trips * (1 + nested); +} + /** The counter of a strip-mined loop: its slot, the LLVM integer type and * signedness of its values, its step, the counters stepped beside it, and * the limit, emitted in the outer header before each block. */ @@ -2645,13 +2722,20 @@ interface StripMinedCounter { * tests the counter against the loop's limit, each block runs the body up * to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll * follows the block. It polls as often as the budget it replaces, and the - * inner loop's only exit besides `break` and `return` is its bound. */ + * inner loop's only exit besides `break` and `return` is its bound. + * + * With `runSlot` (for a loop that another loop of the function encloses), a + * block that ends at the limit leaves the loop without polling, and a run + * of at most WORKER_UNPOLLED_RUN iterations is one block, polled once as + * it leaves when it took more than WORKER_POLL_BLOCK iterations. `runSlot` + * holds whether the current block is such a run. */ function emitStripMinedCountedLoop( emitter: LlEmitter, s: Extract, counter: StripMinedCounter, boundSlot: string, exit: string, + runSlot: string | null, ): void { const B = emitter.B; const { slot, type, signed, step } = counter; @@ -2684,7 +2768,27 @@ function emitStripMinedCountedLoop( `${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`, ); B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`); - B.line(`store ${type} ${bound}, ptr ${boundSlot}`); + if (runSlot === null) B.line(`store ${type} ${bound}, ptr ${boundSlot}`); + else { + // The steps left to the limit: positive, as the header tested. + const left = B.tmp(), + whole = B.tmp(), + long = B.tmp(), + polled = B.tmp(), + end = B.tmp(); + const stride = Math.abs(step); + B.line( + ascending + ? `${left} = sub ${wrap} ${type} ${limit}, ${start}` + : `${left} = sub ${wrap} ${type} ${start}, ${limit}`, + ); + B.line(`${whole} = icmp ule ${type} ${left}, ${stride * WORKER_UNPOLLED_RUN}`); + B.line(`${long} = icmp ugt ${type} ${left}, ${stride * WORKER_POLL_BLOCK}`); + B.line(`${polled} = and i1 ${whole}, ${long}`); + B.line(`${end} = select i1 ${whole}, ${type} ${limit}, ${type} ${bound}`); + B.line(`store ${type} ${end}, ptr ${boundSlot}`); + B.line(`store i1 ${polled}, ptr ${runSlot}`); + } B.br(bc); B.startBlock(bc); const index = B.tmp(); @@ -2693,7 +2797,8 @@ function emitStripMinedCountedLoop( B.line(`${index} = load ${type}, ptr ${slot}`); B.line(`${end} = load ${type}, ptr ${boundSlot}`); B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`); - B.condBr(inBlock, lb, poll); + const after = runSlot === null ? poll : B.newLabel("loop.end"); + B.condBr(inBlock, lb, after); B.startBlock(lb); emitter.jumpTargets.push({ kind: "loop", @@ -2716,11 +2821,45 @@ function emitStripMinedCountedLoop( B.line(`store ${type} ${next}, ptr ${stepSlot}`); } B.br(bc); + if (runSlot !== null) { + // A speculative copy may enter the body without passing the header, so + // the limit is read again here. + B.startBlock(after); + const at = B.tmp(); + const more = B.tmp(); + const run = B.tmp(); + const done = B.newLabel("loop.done"); + const last = B.newLabel("loop.poll.run"); + B.line(`${at} = load ${type}, ptr ${slot}`); + B.line(`${more} = icmp ${compare} ${type} ${at}, ${counter.limit()}`); + B.condBr(more, poll, done); + B.startBlock(done); + B.line(`${run} = load i1, ptr ${runSlot}`); + B.condBr(run, last, exit); + B.startBlock(last); + emitter.emitWorkerPoll(); + B.br(exit); + } B.startBlock(poll); emitter.emitWorkerPoll(); B.br(lc); } +/** Whether `body` holds a loop outside its closures. */ +function containsLoop(body: IrStmt[]): boolean { + const inStmt = (s: IrStmt): boolean => + s.kind === "while" || + s.kind === "doWhile" || + s.kind === "for" || + s.kind === "forOf" || + !everyStmtChild( + s, + () => true, + (child) => !inStmt(child), + ); + return body.some(inStmt); +} + function enterLoopHeader(emitter: LlEmitter, header: string): void { const dispatch = emitter.speculationHeader; emitter.speculationHeader = null; diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 07356cdb9..8cefda8f0 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -1151,6 +1151,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "punycode": case "constants": case "promise": + case "mime": return host.emitGenericLibCall(e); case "test": return emitTestLibCall(host, e); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index fb294aa30..4b3f34b80 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -86,6 +86,17 @@ export const LIB_FN_SYMS: Record = { "util.inherits": "scr_util_inherits", "util.inheritsPrototype": "scr_util_inherits_prototype", "util.deprecate": "scr_util_deprecate", + "util.isArrayDeprecated": "scr_util_is_array_deprecated", + "util.extend": "scr_util_extend", + "util.signalExitCode": "scr_util_signal_exit_code", + "util.aborted": "scr_util_aborted", + "util.diff": "scr_util_diff", + "util.debounce": "scr_util_debounce", + "util.throttle": "scr_util_throttle", + "mime.typeNew": "scr_mime_type_new", + "mime.typeParse": "scr_mime_type_parse", + "mime.paramsNew": "scr_mime_params_new", + "mime.is": "scr_mime_is", "util.callbackify": "scr_util_callbackify", "util.promisifyDyn": "scr_util_promisify", "util.debuglog": "scr_util_debuglog", @@ -1082,6 +1093,26 @@ export const LIB_FN_SYMS: Record = { "dyn.generatorThis": "scr_gen_receiver", "insp.dyn": "scr_insp_dyn", "insp.dynS": "scr_insp_dyn_s", + "insp.format": "scr_insp_format_pack", + "insp.formatPrimitives": "scr_insp_format_primitives", + "insp.formatList": "scr_insp_format_list", + "insp.formatWithOptions": "scr_insp_format_with_options", + "insp.inspect": "scr_insp_inspect", + "insp.customEnabled": "scr_insp_custom_enabled", + "insp.registerCustom": "scr_insp_register_custom", + "insp.customInspect": "scr_insp_custom_inspect", + "insp.depthKind": "scr_insp_depth_kind", + "insp.customDepth": "scr_insp_custom_depth", + "insp.customOptions": "scr_insp_custom_options", + "insp.inspectFunction": "scr_insp_inspect_function", + "insp.customText": "scr_insp_custom_text", + "insp.customIsSelf": "scr_insp_custom_is_self", + "insp.customResult": "scr_insp_custom_result", + "insp.customCallBegin": "scr_insp_custom_call_begin", + "insp.customCallEnd": "scr_insp_custom_call_end", + "insp.formatSpecifies": "scr_insp_format_specifies", + "insp.formatSpecifiesDyn": "scr_insp_format_specifies_dyn", + "insp.dynFormatS": "scr_insp_dyn_format_s", "insp.dynHidden": "scr_insp_dyn_hidden", "fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn", "fs.readFileEncoded": "scr_fs_read_file_encoded", @@ -1742,9 +1773,14 @@ export const USES_TIMERS_LIB_FNS = new Set([ "buffer.deprecation", // The wrappers schedule warnings and callbacks on the tick queue. "util.deprecate", + "util.isArrayDeprecated", + "util.extend", "util.callbackify", "util.promisifyDyn", "util.debuglog", + // The wrapped calls arm timers and settle promises. + "util.debounce", + "util.throttle", "worker.new", "worker.parentPort", "worker.channel", diff --git a/packages/compiler/src/backend/loop-polls.ts b/packages/compiler/src/backend/loop-polls.ts index c582a4974..866711fb7 100644 --- a/packages/compiler/src/backend/loop-polls.ts +++ b/packages/compiler/src/backend/loop-polls.ts @@ -110,8 +110,8 @@ export function loopTestsEveryIteration(loop: Loop, tested: (call: IrExpr) => bo /** True when the body of `loop`, outside its nested loops and closures, * reads or writes a typed-array element: the loops whose cancellation poll * the emitter moves between blocks of iterations, so that LLVM can - * vectorize them. Other loops keep the budget in their body, which costs - * less in a loop of a few iterations. */ + * vectorize them. Outside another loop, other loops keep the budget in + * their body, which costs less in a loop of a few iterations. */ export function loopAccessesTypedArrays(loop: Loop): boolean { const inExpr = (e: IrExpr): boolean => { if (e.kind === "bytesIntrinsic" && e.method === "get") return true; diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index f250a2ac6..b29da246f 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -61,6 +61,10 @@ export interface NativeLinkFeatures { /** events.once with options: scr_events_once_signal.c. Absent (not * false) for programs without it. */ eventsOnceSignal?: boolean; + /** util.MIMEType and util.MIMEParams: scr_mime.c. */ + mime?: boolean; + /** util.debounce and util.throttle: scr_util_timing.c. */ + utilTiming?: boolean; /** Buffer.prototype methods called on checked-dynamic values link * scr_bytes_dyn.c; absent (false) for programs without them. */ dynBuffer?: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index e3f97710f..e144b7f24 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -131,6 +131,10 @@ export interface CcOptions { /** events.once with options (events.onceSignal): compiles * scr_events_once_signal.c. */ eventsOnceSignal?: boolean; + /** util.MIMEType and util.MIMEParams: compiles scr_mime.c. */ + mime?: boolean; + /** util.debounce and util.throttle: compiles scr_util_timing.c. */ + utilTiming?: boolean; /** Buffer.prototype methods called on checked-dynamic values: * compiles scr_bytes_dyn.c. */ dynBuffer?: boolean; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 7f989a5e4..2164368ad 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -249,6 +249,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ // events.once with options registers through the emitter and // event-target units. unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"), + unit((u) => u.opts.mime === true, "scr_mime.c"), + unit((u) => u.opts.utilTiming === true, "scr_util_timing.c"), // The checked-dynamic HANDLE support unit (listener gate + runtime // adapter closures): every referencing unit is one of the emitter or // net families (http implies net), so handle-free binaries keep diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 22bba85d2..730adec96 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -89,6 +89,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { declAdopt: boolean; dynForAwait: boolean; eventsOnceSignal: boolean; + mime: boolean; + utilTiming: boolean; dynBuffer: boolean; builtinValues: boolean; warningReplace: boolean; @@ -289,6 +291,8 @@ export function effectiveRuntimeFeatures( declAdopt: features.declAdopt === true, dynForAwait: features.dynForAwait === true, eventsOnceSignal: features.eventsOnceSignal === true, + mime: features.mime === true, + utilTiming: features.utilTiming === true, dynBuffer: features.dynBuffer === true, builtinValues: features.builtinValues === true, warningReplace: features.warningReplace === true, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index f26c4c89a..ece1dc971 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -76,6 +76,8 @@ export interface EarlyExecutableNativeFeatures { declAdopt?: boolean; dynForAwait?: boolean; eventsOnceSignal?: boolean; + mime?: boolean; + utilTiming?: boolean; dynBuffer?: boolean; builtinValues?: boolean; events: boolean; diff --git a/packages/compiler/src/frontend/lowering/builtins/mime.ts b/packages/compiler/src/frontend/lowering/builtins/mime.ts new file mode 100644 index 000000000..4c36d6cc5 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/mime.ts @@ -0,0 +1,180 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { dynUndefinedExpr, numLit } from "../../../ir/build.js"; +import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; +import { builtinImportOf } from "./module-bindings.js"; + +/* node:util's MIMEType and MIMEParams are native checked-dynamic handles + * (scr_mime.c): both types map to dyn, their getters and setters are + * handle property reads and writes, their methods dispatch through the + * handle, and these helpers lower the uses a dyn value cannot answer by + * itself (construction, MIMEType.parse and instanceof). */ + +type MimeClass = "MIMEType" | "MIMEParams"; + +/** The MIMEType or MIMEParams class in any node:util spelling. */ +export function mimeClassOf(lowerer: Lowerer, node: ts.Expression): MimeClass | null { + const builtin = ts.isIdentifier(node) + ? builtinImportOf(lowerer, node) + : ts.isPropertyAccessExpression(node) && !node.questionDotToken + ? lowerer.builtinMemberOf(node) + : null; + if (builtin?.module !== "util") return null; + return builtin.member === "MIMEType" || builtin.member === "MIMEParams" ? builtin.member : null; +} + +/** The class of a stdlib MIMEType- or MIMEParams-typed value. */ +function mimeValueClass(lowerer: Lowerer, node: ts.Expression): MimeClass | null { + const type = lowerer.typeOf(node); + const sym = type.getAliasSymbol() ?? type.getSymbol(); + if (!sym) return null; + const name = ts.symbolName(sym); + if (name !== "MIMEType" && name !== "MIMEParams") return null; + return lowerer.isStdlibSymbol(sym) ? name : null; +} + +/** The call of `fn` over the first `count` arguments, as checked-dynamic + * values (missing ones as undefined). Every argument evaluates in order + * before the call, as in JavaScript: with surplus arguments, the used ones + * are held in hidden locals while the others evaluate. */ +function mimeCall( + lowerer: Lowerer, + fn: IrLibFn, + nodes: readonly ts.Expression[], + count: number, + loc: ReturnType, +): IrExpr { + const values = nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + if (values.length <= count) { + const args = [...values]; + while (args.length < count) args.push(dynUndefinedExpr(loc)); + return { kind: "libCall", fn, args, type: DYN, loc }; + } + const locals = values.slice(0, count).map((value) => ({ + value, + local: lowerer.declareHiddenLocal("%mimeArg", DYN), + })); + return { + kind: "seqExpr", + stmts: [ + ...locals.map(({ value, local }) => ({ + kind: "varDecl" as const, + localId: local.id, + init: value, + loc, + })), + ...values.slice(count).map((arg) => ({ kind: "exprStmt" as const, expr: arg, loc: arg.loc })), + ], + result: { + kind: "libCall", + fn, + args: locals.map(({ local }): IrExpr => ({ + kind: "varRef", + localId: local.id, + type: DYN, + loc, + })), + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + +/** `new MIMEType(string[, noThrow])` and `new MIMEParams()`. */ +export function lowerMimeNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + const cls = mimeClassOf(lowerer, expr.expression); + if (cls === null) return null; + const nodes = expr.arguments?.nodes ?? []; + if (nodes.some(ts.isSpreadElement)) return null; + const loc = locOf(expr); + const fn: IrLibFn = cls === "MIMEType" ? "mime.typeNew" : "mime.paramsNew"; + return mimeCall(lowerer, fn, nodes, cls === "MIMEType" ? 2 : 0, loc); +} + +const INSTANCE_METHODS: Record> = { + MIMEType: new Set(["toString", "toJSON"]), + MIMEParams: new Set([ + "get", + "has", + "set", + "delete", + "entries", + "keys", + "values", + "toString", + "toJSON", + ]), +}; + +/** MIMEType.parse(), and the instance methods, which the handle answers + * as methods, so they dispatch through dynInvoke rather than as a member + * read and a call. */ +export function lowerMimeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; + const access = call.expression; + if (access.questionDotToken || call.arguments.nodes.some(ts.isSpreadElement)) return null; + const name = access.name.text; + const loc = locOf(call); + if (name === "parse" && mimeClassOf(lowerer, access.expression) === "MIMEType") { + return mimeCall(lowerer, "mime.typeParse", call.arguments.nodes, 1, loc); + } + const cls = mimeValueClass(lowerer, access.expression); + if (cls === null || !INSTANCE_METHODS[cls].has(name)) return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: name, + calleeName: ts.getTextOfNode(access), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc, + }; +} + +/** Reports SC2020 when `access` reads a static member of MIMEType or + * MIMEParams (`MIMEType.prototype`, `MIMEType.name`); null otherwise. + * JavaScript sources hold either class as an identity token, which has no + * members, so the read would answer undefined. MIMEType.parse calls lower + * in lowerMimeCall and never reach the member read. */ +export function fenceMimeStaticRead(lowerer: Lowerer, access: ts.PropertyAccessExpression): null { + const cls = mimeClassOf(lowerer, access.expression); + if (cls !== null) lowerer.noLowering(`util.${cls}.${access.name.text}`, access); + return null; +} + +/** `v instanceof MIMEType` / `v instanceof MIMEParams`. */ +export function lowerMimeInstanceof(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null { + const cls = mimeClassOf(lowerer, expr.right); + if (cls === null) return null; + const loc = locOf(expr); + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { + kind: "libCall", + fn: "mime.is", + args: [value, numLit(cls === "MIMEType" ? 0 : 1, loc)], + type: BOOL, + loc, + }; +} + +/** `params`, `params.keys()`, `params.values()` or `params.entries()` over a + * MIMEParams: its checked-dynamic view iterates the live entry list, like + * URLSearchParams (isSearchParamsIterable). */ +export function isMimeParamsIterable(lowerer: Lowerer, node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (mimeValueClass(lowerer, node) === "MIMEParams") return true; + if ( + !ts.isCallExpression(node) || + node.arguments.nodes.length !== 0 || + !ts.isPropertyAccessExpression(node.expression) + ) + return false; + const name = node.expression.name.text; + return ( + (name === "keys" || name === "values" || name === "entries") && + mimeValueClass(lowerer, node.expression.expression) === "MIMEParams" + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index 2aaa2f80f..b21c964f9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -29,11 +29,13 @@ import { checkFsUrlPaths, isFsPathParam, lowerFallibleFsUrlCall, lowerFsPath } f import { lowerBufferEncodingCall } from "./text-codecs.js"; import { lowerUtilTypeCall } from "./util-types.js"; import { InternalCompilerError } from "../../../errors.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { type BuiltinModuleFn } from "../surfaces.js"; import { type ParamShape } from "../call-signatures.js"; -import { BYTES_U8, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js"; +import { BYTES_U8, DYN, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js"; +import { typeKey } from "../../type-mapper.js"; import { lowerProcessLoadEnvFile } from "./environment.js"; import { lowerFsWatchCall, lowerFsReaddirTypesCall, lowerFsStatOptionsCall } from "./filesystem.js"; import { lowerZlibModuleCall } from "./compression.js"; @@ -386,7 +388,65 @@ export function lowerBuiltinModuleCall( const lowered = lowerFsReadPathCall(lowerer, expr, loc); if (lowered) return lowered; } - const required = fn.params.length - (fn.defaults?.length ?? 0); + // util._extend over a typed target: the writes go through its + // checked-dynamic view, and the call answers the target itself, as Node + // does, when the result type is the target's. + if ( + bi.module === "util" && + bi.member === "_extend" && + expr.arguments.nodes.length >= 1 && + !expr.arguments.nodes.some(ts.isSpreadElement) + ) { + const target = lowerer.lowerExpr(expr.arguments.nodes[0]!); + const resultType = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if ( + target.type.kind !== "dyn" && + resultType !== null && + typeKey(resultType) === typeKey(target.type) + ) { + const held = lowerer.declareHiddenLocal("%extendTarget", target.type); + const ref = varRef(held.id, target.type, loc); + const source = expr.arguments.nodes[1] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1], DYN) + : dynUndefinedExpr(loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: held.id, init: target, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "util.extend", + args: [lowerer.coerceToExpected(ref, DYN), source], + type: DYN, + loc, + }, + loc, + }, + ], + result: ref, + type: target.type, + loc, + }; + } + } + // Trailing checked-dynamic parameters whose value adapter defaults to + // undefined may be omitted: the call passes undefined, as Node sees it. + let undefinedTail = 0; + if (!fn.defaults && fn.valueParams) { + for (let i = fn.valueParams.length - 1; i >= 0; i--) { + const param = fn.valueParams[i]!; + if ( + param.mode !== "optional" || + param.defaultValue !== undefined || + fn.params[i]?.kind !== "dyn" + ) + break; + undefinedTail++; + } + } + const required = fn.params.length - (fn.defaults?.length ?? undefinedTail); const hasSpread = expr.arguments.nodes.some(ts.isSpreadElement); if ( bi.module === "url" && @@ -466,12 +526,9 @@ export function lowerBuiltinModuleCall( : { type, mode: "omittable", - callDefault: { - kind: "strLit", - value: fn.defaults![i - required]!, - type: STRING, - loc, - }, + callDefault: fn.defaults + ? { kind: "strLit", value: fn.defaults[i - required]!, type: STRING, loc } + : dynUndefinedExpr(loc), }, ); args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr); @@ -482,7 +539,11 @@ export function lowerBuiltinModuleCall( : lowerer.lowerExprExpecting(a, fn.params[i]), ); for (let i = args.length; i < fn.params.length; i++) { - const dflt = fn.defaults![i - required]!; + if (!fn.defaults) { + args.push(dynUndefinedExpr(loc)); + continue; + } + const dflt = fn.defaults[i - required]!; args.push({ kind: "strLit", value: dflt, type: STRING, loc }); } } diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index b4dbbf513..d3378c8ec 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,7 +1,8 @@ import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { varRef } from "../../../ir/build.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { isJsSourceFile } from "../../program.js"; import { BOOL, DYN, @@ -92,12 +93,22 @@ function callbackShape( return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error }; } -/** Whether `promisify(fn)`'s one argument is typed `any`: the function is - * only known at run time, so the promisified function is built there. */ +/** Whether `promisify(fn)`'s one argument is only known at run time, so + * the promisified function is built there: a value typed `any`, a function + * with untyped parameters (the declarations type that call `any`), or any + * argument in JavaScript, whose inferred types do not describe the + * function Node receives (it validates the argument and reads its own + * properties). */ export function untypedPromisifyArgument(lowerer: Lowerer, call: ts.CallExpression): boolean { - if (call.questionDotToken || call.arguments.nodes.length !== 1) return false; - const argNode = call.arguments.nodes[0]!; - return !ts.isSpreadElement(argNode) && (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0; + if (call.questionDotToken || call.arguments.nodes.length > 1) return false; + const argNode = call.arguments.nodes[0]; + if (argNode !== undefined && ts.isSpreadElement(argNode)) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(call))) return true; + if (argNode === undefined) return false; + return ( + (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 || + (lowerer.typeOf(call).flags & ts.TypeFlags.Any) !== 0 + ); } /** The callback shape of a `promisify(fn)` call, or null. */ @@ -144,10 +155,11 @@ export function lowerCallbackPromisify( if (shape === null) { // An untyped (`any`) function: Node's promisify at run time. if (!untypedPromisifyArgument(lowerer, call)) return null; + const argNode = call.arguments.nodes[0]; return { kind: "libCall", fn: "util.promisifyDyn", - args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], + args: [argNode ? lowerer.lowerExprExpecting(argNode, DYN) : dynUndefinedExpr(loc)], type: DYN, loc, }; diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 79e891d4f..797bb1775 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -57,6 +57,95 @@ function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean { return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0; } +/** A call's arguments as one checked-dynamic array, built in source order + * into a fresh hidden local: each argument converts to dyn, and a spread + * contributes the values its iteration yields (holes as undefined). + * `lowerSpread` lowers a spread's operand; the default lowers it here. */ +export function dynArgumentPack( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, + options: { + /** Pre-lowered values pushed before the arguments. */ + leading?: readonly IrExpr[] | undefined; + blame?: ts.Node | undefined; + lowerSpread?: (arg: ts.SpreadElement) => IrExpr; + } = {}, +): { stmts: IrStmt[]; pack: () => IrExpr } { + const blame = options.blame ?? argNodes[0]!; + const lowerSpread = + options.lowerSpread ?? ((arg: ts.SpreadElement) => lowerer.lowerExpr(arg.expression)); + const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN); + const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc }); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: pack.id, + init: { kind: "dynArrLit", elems: [], type: DYN, loc }, + loc, + }, + ]; + const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => { + stmts.push({ + kind: "exprStmt", + expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc }, + loc: value.loc, + }); + }; + for (const value of options.leading ?? []) + push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush"); + const spreads = argNodes.filter(ts.isSpreadElement).length; + argNodes.forEach((arg, index) => { + if (!ts.isSpreadElement(arg)) { + push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush"); + return; + } + let value = lowerSpread(arg); + if (value.type.kind === "set") { + value = { + kind: "setIntrinsic", + method: "toArray", + receiver: value, + args: [], + type: arrayOf(value.type.elem), + loc: value.loc, + }; + } else if (value.type.kind === "object") { + value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value; + } + if (value.type.kind === "array") { + // Argument iteration materializes holes as present undefined + // before conversion to the dense checked-dynamic argument pack. + value = { + kind: "arrayLit", + elems: [value], + spreads: [0], + type: value.type, + loc: value.loc, + }; + } + value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc); + if (spreads === 1 && index === argNodes.length - 1) { + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.packPushSpread", + args: [ + ref(), + value, + { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }); + } else push(value, "dyn.packPushSpreadIter"); + }); + return { stmts, pack: ref }; +} + export function completeArgs( lowerer: Lowerer, argNodes: readonly ts.Expression[], @@ -97,72 +186,10 @@ export function completeArgs( // Runtime-length spreads determine the complete argument list before // any parameter default runs. Build it once in source order, then // extract the fixed native ABI slots; missing elements are undefined. - const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN); - const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc }); - const stmts: IrStmt[] = [ - { - kind: "varDecl", - localId: pack.id, - init: { kind: "dynArrLit", elems: [], type: DYN, loc }, - loc, - }, - ]; - const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => { - stmts.push({ - kind: "exprStmt", - expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc }, - loc: value.loc, - }); - }; - for (const value of leading ?? []) push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush"); - const spreads = argNodes.filter(ts.isSpreadElement).length; - argNodes.forEach((arg, index) => { - if (!ts.isSpreadElement(arg)) { - push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush"); - return; - } - let value = lowerSpread(arg); - if (value.type.kind === "set") { - value = { - kind: "setIntrinsic", - method: "toArray", - receiver: value, - args: [], - type: arrayOf(value.type.elem), - loc: value.loc, - }; - } else if (value.type.kind === "object") { - value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value; - } - if (value.type.kind === "array") { - // Argument iteration materializes holes as present undefined - // before conversion to the dense checked-dynamic argument pack. - value = { - kind: "arrayLit", - elems: [value], - spreads: [0], - type: value.type, - loc: value.loc, - }; - } - value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc); - if (spreads === 1 && index === argNodes.length - 1) { - stmts.push({ - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.packPushSpread", - args: [ - ref(), - value, - { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc }, - ], - type: VOID, - loc, - }, - loc, - }); - } else push(value, "dyn.packPushSpreadIter"); + const { stmts, pack: ref } = dynArgumentPack(lowerer, argNodes, loc, { + leading, + blame, + lowerSpread, }); const args = shapes.map((shape, index): IrExpr => { if (shape.mode === "arguments") return ref(); diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index bfaca6f96..21cc36016 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -44,6 +44,7 @@ import { } from "../collection-ingestion.js"; import { strCharsCall } from "./array-construction.js"; import { arrayElementRead } from "./array-iteration.js"; +import { isMimeParamsIterable } from "../builtins/mime.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { classGeneratorIterable } from "../class-iterables.js"; import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js"; @@ -1271,8 +1272,8 @@ export function lowerMapSeedNew( ); } // new Map(sp) / new Map(sp.entries()): the checked-dynamic view's live - // pairs seed the map in order. - if (isSearchParamsIterable(lowerer, argNode)) { + // pairs seed the map in order, and so do a MIMEParams's entries. + if (isSearchParamsIterable(lowerer, argNode) || isMimeParamsIterable(lowerer, argNode)) { return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT); } const input = collectionInput(lowerer, argNode); diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 77c16594f..5854a0ae8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -29,6 +29,7 @@ import { lowerTernary } from "./narrowing.js"; import { lowerCollectionSpread } from "../containers/collection-methods.js"; import { strCharsCall } from "../containers/array-construction.js"; import { generatorDrain } from "../iterator-adapters.js"; +import { isMimeParamsIterable } from "../builtins/mime.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { arrayValueStore, primitiveElement } from "../array-values.js"; @@ -543,7 +544,9 @@ export function lowerArrayLiteral( // `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its // checked-dynamic view iterates the live pair list, and the drained // [name, value] pairs convert to the literal's tuple elements. - const searchParamsSource = isSearchParamsIterable(lowerer, el.expression); + const searchParamsSource = + isSearchParamsIterable(lowerer, el.expression) || + isMimeParamsIterable(lowerer, el.expression); if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN); // A native checked iterable can supply scalar elements even when // the checker inferred a typed array from its producer. Drain once diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 4e46b4d5f..b0dcef332 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1551,6 +1551,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr reference.kind === "map" || reference.kind === "set" || reference.kind === "union" || + // A promise of dyn boxes its own promise, so identity holds; + // other promise types box a fresh adapter. + (reference.kind === "promise" && reference.inner.kind === "dyn") || DYN_HANDLE_KINDS.has(reference.kind)) && lowerer.dynConvertible(reference) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 70766949a..f57e52159 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -34,7 +34,11 @@ import { } from "../../type-mapper.js"; import { isSafeToRepeat } from "./evaluation-safety.js"; import { tryLowerExpression } from "./try-lower-expression.js"; -import { fenceSymbolFieldCopy } from "../symbol-fields.js"; +import { + classSymbolKeyOf, + fenceSymbolFieldCopy, + symbolFieldDisplayName, +} from "../symbol-fields.js"; import { planUnionRetag } from "../union-retag.js"; import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js"; import { builtinGlobalValue } from "./property-access.js"; @@ -197,7 +201,19 @@ function propNameText(lowerer: Lowerer, name: ts.PropertyName): string { * (NamedEvaluation): an identifier, string or numeric key, or a computed * key that folds to one string. Null for other keys. */ function propertyFunctionName(lowerer: Lowerer, name: ts.PropertyName): string | null { - if (ts.isComputedPropertyName(name)) return literalComputedKey(lowerer, name); + if (ts.isComputedPropertyName(name)) { + const literal = literalComputedKey(lowerer, name); + if (literal !== null) return literal; + // A symbol key names the function `[description]`. + let key = name.expression; + while (ts.isParenthesizedExpression(key)) key = key.expression; + const symbol = classSymbolKeyOf(lowerer, key); + if (symbol) return `[${symbolFieldDisplayName(symbol.fieldName).slice("Symbol(".length, -1)}]`; + const wellKnown = ts.isPropertyAccessExpression(key) + ? lowerer.stdlibGlobalMember(key, "Symbol") + : null; + return wellKnown ? `[Symbol.${wellKnown}]` : null; + } if (ts.isNumericLiteral(name)) return String(Number(name.text)); if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text; return null; diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index e3edf8bba..6466857a5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -9,6 +9,7 @@ import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; import { typedArgumentsArity } from "../typed-arguments.js"; +import { INSPECT_CUSTOM_DESCRIPTION, isInspectCustomAccess } from "../symbol-fields.js"; import { isThisParameter } from "../call-signatures.js"; import { argumentCountSlot, contextualCallCount, usedAsValue } from "../argument-counts.js"; import { @@ -64,6 +65,7 @@ import { } from "../../../ir/ir.js"; import { lowerNavigatorMember } from "../builtins/navigator.js"; import { lowerX509PublicKey } from "../builtins/x509.js"; +import { fenceMimeStaticRead } from "../builtins/mime.js"; import { NODE_COMPAT_VERSION } from "../builtins/process.js"; import { inlineNamespaceRequireOf, @@ -338,6 +340,16 @@ export function lowerPropertyAccessExpr( // Cloudflare probe) folds like `navigator.userAgent`. const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; + // util.inspect.custom: Symbol.for('nodejs.util.inspect.custom'). + if (isInspectCustomAccess(lowerer, expr)) { + return { + kind: "libCall", + fn: "sym.for", + args: [{ kind: "strLit", value: INSPECT_CUSTOM_DESCRIPTION, type: STRING, loc }], + type: SYMBOL_T, + loc, + }; + } { const member = lowerBufferPrototypeMember(lowerer, expr); if (member) return member; @@ -934,6 +946,7 @@ export function lowerPropertyAccessExpr( // prototype that no lowering above claimed keeps the SC2020 fence // rather than reading undefined through the field and dynamic reads. fenceBuiltinConstructorStaticRead(lowerer, expr) ?? + fenceMimeStaticRead(lowerer, expr) ?? lowerer.lowerFieldRead(expr); // A union-typed field read narrows like an identifier when the // checker has narrowed this use to one arm. diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 389abb521..b323162c0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -39,6 +39,7 @@ import { lowerReadlineInstanceof } from "../builtins/readline.js"; import { lowerKeyObjectInstanceof } from "../builtins/key-object.js"; import { lowerCryptoKeyInstanceof } from "../builtins/webcrypto.js"; import { lowerNetAddressInstanceof } from "../builtins/net-address.js"; +import { lowerMimeInstanceof } from "../builtins/mime.js"; import { typedArrayHasMember } from "../builtins/typed-array-members.js"; import { fenceEarlyNsMemberRef, nsMemberIdentOf } from "../lower-namespaces.js"; import { maybePromiseInstanceOf } from "../maybe-promise.js"; @@ -246,6 +247,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (cryptoKey) return cryptoKey; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; + const mime = lowerMimeInstanceof(lowerer, expr); + if (mime) return mime; const readlineInterface = lowerReadlineInstanceof(lowerer, expr); if (readlineInterface) return readlineInterface; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index aeb1993d4..0a845b3bd 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -284,6 +284,28 @@ import { lowerRecordInheritedCall } from "./record-inherited.js"; const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); +/** The node:util member a `(0, member)` callee names, or null. */ +function indirectUtilCalleeOf( + lowerer: Lowerer, + callee: ts.Expression, +): { module: string; member: string } | null { + let e = callee; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if ( + !ts.isBinaryExpression(e) || + e.operatorToken.kind !== ts.SyntaxKind.CommaToken || + !ts.isNumericLiteral(e.left) + ) + return null; + const target = e.right; + const bi = ts.isIdentifier(target) + ? lowerer.builtinImportOf(target) + : ts.isPropertyAccessExpression(target) && !target.questionDotToken + ? lowerer.builtinMemberOf(target) + : null; + return bi?.module === "util" ? bi : null; +} + /** `Object.defineProperty(target, ...)`, `Object.defineProperties`, * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` * over a target that may hold an Error can install an accessor over an @@ -377,6 +399,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression); if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration); } + // `(0, util_1.inspect)(value)`: TypeScript's CommonJS output calls a + // named import through a comma expression, which leaves the call's + // receiver undefined. node:util's functions do not read their receiver, + // so the call lowers as the direct one. + { + const util = indirectUtilCalleeOf(lowerer, expr.expression); + if (util) return lowerBuiltinCall(lowerer, expr, util, loc); + } // The deprecated Buffer(...) call (containers/bytes.ts). The checker // types it `any`, so the bytes cross as a checked-dynamic value, which // keeps the Buffer brand for inspection. diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 5f1f6a03d..d941d7b60 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -163,6 +163,7 @@ import { } from "./lower-event-emitter.js"; import { declSymbolOf } from "./lower-modules.js"; import { + INSPECT_CUSTOM_MEMBER, classSymbolKeyOf, classSymbolKeyOfSymbol, symbolFieldDisplayName, @@ -4454,6 +4455,9 @@ export function collectClassShapeInner( ...(methods.size > 0 ? { methods: [...methods.keys()] } : {}), ...((): { prototypeNames?: string[] } => { const names = classPrototypeNames(decl); + // A [util.inspect.custom] method, by its reserved slot name: the + // checked-dynamic inspect walk refuses instances that have one. + if (methods.has(INSPECT_CUSTOM_MEMBER)) names.push(INSPECT_CUSTOM_MEMBER); return names.length > 0 ? { prototypeNames: names } : {}; })(), ...(abstractClass ? { abstract: true as const } : {}), diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index b45218bd0..5632f7402 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -14,7 +14,15 @@ import { boolLit, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/buil import { locOf } from "../program.js"; import { type ConsoleMethodMember } from "./builtins/console.js"; import { jsFuncNameOf, type Lowerer } from "./lowerer.js"; -import { lowerConsoleInspectArg, lowerFormatCall, lowerInspectValue } from "./lower-inspect.js"; +import { + heldFormatArgs, + lowerConsoleInspectArg, + lowerFormatCall, + lowerInspectValue, + lowerRuntimeFormat, + runtimeFormatString, + formatStringConverts, +} from "./lower-inspect.js"; import { streamSidesOf } from "./lower-stream.js"; /** node:console Console instances: a native handle (the checked-dynamic @@ -358,9 +366,32 @@ export function lowerConsoleOutput( ) { return consoleWrite(stdout, [lowerFormatCall(lowerer, expr, loc, false)], loc); } - const args = expr.arguments.nodes.map((a) => - lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc), - ); + const nodes = expr.arguments.nodes; + // Spread arguments and format strings that may hold a substitution at + // run time go through the runtime formatter. A string first argument + // without a '%' keeps the space-joined write below. + if (nodes.some(ts.isSpreadElement)) + return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); + const runtime = + nodes.length > 1 && !isStringLiteralLike(first) ? runtimeFormatString(lowerer, first!) : null; + if (runtime === "other" && !formatStringConverts(lowerer, first!)) + return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc); + if (runtime !== null) { + const held = heldFormatArgs(lowerer, nodes, loc); + if (!held.joinable) return seq(held.stmts, consoleWrite(stdout, [held.format()], loc), loc); + const joined = held.args.map((arg) => + lowerConsoleArg(lowerer, arg.node, arg.ref(), surface, loc), + ); + const write: IrStmt = { + kind: "if", + cond: held.specifies, + then: [{ kind: "exprStmt", expr: consoleWrite(stdout, [held.format()], loc), loc }], + else_: [{ kind: "exprStmt", expr: consoleWrite(stdout, joined, loc), loc }], + loc, + }; + return seq([...held.stmts, write], noop(loc), loc); + } + const args = nodes.map((a) => lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc)); return consoleWrite(stdout, args, loc); } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index bd9b5f55d..7f4c8dfb7 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -167,6 +167,23 @@ function declareFunctionArguments( } } +/** The type parameters a lambda without its own takes from a generic + * contextual signature, unbound in the current scope. */ +function contextualTypeParameterSymbols( + lowerer: Lowerer, + node: ts.ArrowFunction | ts.FunctionExpression, +): ts.Symbol[] { + const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node)); + if (signatures.length !== 1) return []; + const symbols: ts.Symbol[] = []; + for (const parameter of signatures[0]!.getTypeParameters()) { + const symbol = parameter.getSymbol(); + if (!symbol || lowerer.typeParamBindings?.has(symbol)) return []; + symbols.push(symbol); + } + return symbols; +} + /** Lifts an arrow function / function expression / nested declaration / * object-literal shorthand method to a module-level function and yields * the `closure` expression creating it. */ @@ -202,6 +219,31 @@ export function lowerLambda( lowerer.typeParamBindings = previous; } } + // A lambda contextually typed by a GENERIC signature (`send: (m) => …` + // for `send(m: Msg): string`) takes that signature's type + // parameters as its own; it lowers once, erased, like a generic lambda. + if ( + (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && + !node.typeParameters?.nodes.length && + !lowerer.erasingLambdas.has(node) + ) { + const contextual = contextualTypeParameterSymbols(lowerer, node); + if (contextual.length > 0) { + const previous = lowerer.typeParamBindings; + const bindings = new Map(previous ?? []); + for (const symbol of contextual) bindings.set(symbol, DYN); + lowerer.typeParamBindings = bindings; + lowerer.erasingLambdas.add(node); + lowerer.erasingDepth++; + try { + return lowerLambda(lowerer, node, checkedReceiver); + } finally { + lowerer.erasingDepth--; + lowerer.erasingLambdas.delete(node); + lowerer.typeParamBindings = previous; + } + } + } const prepared = ts.isArrowFunction(node) || ts.isFunctionExpression(node) ? lowerer.preparedClassFactories.get(node) diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 1ff19782c..e14c3efca 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -17,7 +17,8 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { unsignedHex } from "../../format-integer.js"; import { jsFuncNameOf, type Lowerer } from "./lowerer.js"; -import { isJsSourceFile } from "../program.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { noLoweringDiag } from "../../diagnostics/diagnostic.js"; import { BOOL, CAUGHT, @@ -25,6 +26,7 @@ import { F64, type IrExpr, type IrFunction, + type IrLocal, type IrStmt, type IrStrIntrinsicMethod, type IrType, @@ -36,6 +38,8 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + funcOf, + arrayOf, CLASS_FIELD_ATTRIBUTES_PREFIX, classDynViewSupported, DYN_CLASS_PROPERTIES, @@ -49,13 +53,18 @@ import { } from "../../ir/ir.js"; import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; -import { symbolFieldDisplayName, TO_STRING_TAG_FIELD } from "./symbol-fields.js"; +import { + INSPECT_CUSTOM_MEMBER, + symbolFieldDisplayName, + TO_STRING_TAG_FIELD, +} from "./symbol-fields.js"; import { eventTargetRootOf } from "./lower-event-target.js"; import { dateRootOf } from "./builtins/date-classes.js"; import { lowerSystemErrorMap } from "./lower-system-errors.js"; import { isPerfEntryType, perfEntryJson } from "./builtins/performance.js"; import { recordMayKeepSource, recordSourceTest } from "./record-sources.js"; import { nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js"; +import { dynArgumentPack } from "./call-arguments.js"; /* ── IR construction shorthand ───────────────────────────────────────── */ function concatAll(parts: IrExpr[], loc: SrcLoc): IrExpr { @@ -806,6 +815,8 @@ function inspectSupport( case "object": { const info = lowerer.classes.get(t.className); if (!info) return `class '${t.className}' has no inspect lowering`; + const custom = customInspectReason(lowerer, info); + if (custom !== null) return custom; // EventTarget and Event subclasses render through Node's inspectors // for those classes (the runtime's capsule view), and so do classes // declared `extends Date`. @@ -904,8 +915,10 @@ export function inspectExpr( recurse: IrExpr, depth: IrExpr, loc: SrcLoc, + /** The instance's own rendering, without its custom inspect method. */ + ownRendering = false, ): IrExpr { - const render = inspectExprBody(lowerer, t, value, recurse, depth, loc); + const render = inspectExprBody(lowerer, t, value, recurse, depth, loc, ownRendering); // A top-level render through emitted helpers that can reach a refusal // (the 'not supported yet' Error of insp.dyn) unwinds the inspector to // its state before the render on every exit: the Error propagates out of @@ -1015,6 +1028,7 @@ function inspectExprBody( recurse: IrExpr, depth: IrExpr, loc: SrcLoc, + ownRendering: boolean, ): IrExpr { // A Dirent row renders through its dyn view: Node's `Dirent { ... }` // with the Symbol(type) key. @@ -1095,6 +1109,15 @@ function inspectExprBody( loc, }; case "object": + if (!ownRendering && customInspectMethodOf(lowerer, t.className)) { + return { + kind: "call", + callee: customInspectHelper(lowerer, t, loc), + args: [value, recurse, depth], + type: STRING, + loc, + }; + } if ( eventTargetRootOf(lowerer.classes.get(t.className)) !== null || dateRootOf(lowerer.classes.get(t.className)) !== null @@ -1274,6 +1297,297 @@ function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolea ); } +/* ── custom inspect methods ([util.inspect.custom]) ──────────────────── */ + +/** The `[util.inspect.custom]` method a class's instances have, declared + * on the class or a base; null when they have none. */ +function customInspectMethodOf(lowerer: Lowerer, className: string) { + const info = lowerer.classes.get(className); + return info ? (lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER) ?? null) : null; +} + +/** Why instances of a class with a custom inspect method have no static + * rendering, or null. Node calls the method with the remaining depth, the + * options and util.inspect; the method's parameters receive them as + * checked-dynamic values, and its result renders in the instance's place. */ +function customInspectReason(lowerer: Lowerer, info: ClassInfo): string | null { + const found = lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER); + if (!found) { + const below = (c: ClassInfo): boolean => + c.subclasses.some((s) => s.methods.has(INSPECT_CUSTOM_MEMBER) || below(s)); + return below(info) + ? "classes whose subclasses declare [util.inspect.custom] have no inspect lowering yet (declare the method on the class itself)" + : null; + } + if (found.sig.params.length > 3) + return "[util.inspect.custom] methods with more than three parameters have no inspect lowering"; + for (const param of found.sig.params) { + if ( + param.type.kind !== "dyn" && + !canDynCheckTo( + param.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) + return `[util.inspect.custom] methods with a '${lowerer.fmt(param.type)}' parameter have no inspect lowering`; + } + const ret = found.sig.ret; + if (ret.kind === "object") + return "[util.inspect.custom] methods that return a class instance have no inspect lowering"; + if ( + ret.kind !== "string" && + ret.kind !== "dyn" && + ret.kind !== "void" && + inspectSupport(lowerer, ret, new Set(), { recursive: false }) !== null + ) + return `[util.inspect.custom] methods returning '${lowerer.fmt(ret)}' have no inspect lowering`; + return null; +} + +/** Whether values of `t` can hold a checked-dynamic value, whose own custom + * inspect functions the runtime walk calls. */ +function typeReachesDyn(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + if (u.kind === "dyn") return true; + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +/** Whether values of `t` can hold an instance of a class with a custom + * inspect method. */ +export function typeReachesCustomInspect(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + if (u.kind === "object" && customInspectMethodOf(lowerer, u.className)) return true; + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +/** `%util.inspCustom.(v, r, d): string` — formatValue's custom hook + * over a class whose instances have a `[util.inspect.custom]` method: + * with ctx.customInspect set, the method's result (a string re-indented + * at the current level, the instance itself rendered normally, any other + * value rendered in its place); otherwise the instance's own rendering. */ +function customInspectHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + loc: SrcLoc, +): string { + const key = `inspCustom:${typeKey(t)}`; + const existing = lowerer.inspectHelpers.get(key); + if (existing) return existing; + const name = keyedHelperName(lowerer, "%util.inspCustom", key); + lowerer.inspectHelpers.set(key, name); + const found = customInspectMethodOf(lowerer, t.className)!; + const v = (): IrExpr => varRef("v.0", t, loc); + const r = (): IrExpr => varRef("r.0", F64, loc); + const d = (): IrExpr => varRef("d.0", F64, loc); + const ret = (value: IrExpr): IrStmt => ({ kind: "return", value, loc }); + const normal = (): IrExpr => inspectExpr(lowerer, t, v(), r(), d(), loc, true); + const runtimeArgs: IrExpr[] = [ + { kind: "libCall", fn: "insp.customDepth", args: [r(), d()], type: DYN, loc }, + { kind: "libCall", fn: "insp.customOptions", args: [d()], type: DYN, loc }, + { kind: "libCall", fn: "insp.inspectFunction", args: [], type: DYN, loc }, + ]; + const locals: IrLocal[] = [ + { id: "v.0", name: "v", type: t, mutable: false }, + { id: "r.0", name: "r", type: F64, mutable: false }, + { id: "d.0", name: "d", type: F64, mutable: false }, + ]; + // The arguments read the walk's depth and options, so they evaluate + // before the method's own context begins. + const argDecls: IrStmt[] = found.sig.params.map((param, i) => { + locals.push({ id: `arg${i}.0`, name: `arg${i}`, type: param.type, mutable: false }); + const init = lowerer.coerceToExpected(runtimeArgs[i]!, param.type); + return { kind: "varDecl", localId: `arg${i}.0`, init, loc }; + }); + const args = found.sig.params.map((param, i) => varRef(`arg${i}.0`, param.type, loc)); + const resultT = found.sig.ret; + const call = accessorCall(lowerer, t.className, INSPECT_CUSTOM_MEMBER, v(), args, resultT, loc); + // The method runs in a fresh inspect context (insp.customCallBegin); the + // walk's own context returns before the result renders. The token lets + // an enclosing end restore this walk's context when the method throws. + locals.push({ id: "tok.0", name: "tok", type: F64, mutable: false }); + const end: IrStmt = { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.customCallEnd", + args: [varRef("tok.0", F64, loc)], + type: VOID, + loc, + }, + loc, + }; + const render: IrStmt[] = [ + ...argDecls, + { + kind: "varDecl", + localId: "tok.0", + init: { kind: "libCall", fn: "insp.customCallBegin", args: [], type: F64, loc }, + loc, + }, + ]; + if (resultT.kind === "void") { + render.push({ kind: "exprStmt", expr: call, loc }, end); + } else { + locals.push({ id: "res.0", name: "res", type: resultT, mutable: false }); + render.push({ kind: "varDecl", localId: "res.0", init: call, loc }, end); + } + const res = (): IrExpr => varRef("res.0", resultT, loc); + if (resultT.kind === "string") { + render.push(ret({ kind: "libCall", fn: "insp.customText", args: [res()], type: STRING, loc })); + } else if (resultT.kind === "dyn") { + render.push( + { + kind: "if", + cond: { + kind: "libCall", + fn: "insp.customIsSelf", + args: [res(), { kind: "dynFrom", value: v(), liveRef: true, type: DYN, loc }], + type: BOOL, + loc, + }, + then: [ret(normal())], + else_: null, + loc, + }, + ret({ kind: "libCall", fn: "insp.customResult", args: [res(), r(), d()], type: STRING, loc }), + ); + } else if (resultT.kind === "void") { + render.push(ret(strLit("undefined", loc))); + } else { + render.push(ret(inspectExpr(lowerer, resultT, res(), r(), d(), loc))); + } + lowerer.liftedFns.push({ + name, + params: [ + { localId: "v.0", name: "v", type: t }, + { localId: "r.0", name: "r", type: F64 }, + { localId: "d.0", name: "d", type: F64 }, + ], + returnType: STRING, + locals, + body: [ + { + kind: "if", + cond: { kind: "libCall", fn: "insp.customEnabled", args: [], type: BOOL, loc }, + then: render, + else_: null, + loc, + }, + ret(normal()), + ], + loc, + }); + return name; +} + +/** `%util.inspCustomDyn.(self, depth, options, inspect): dyn` — a + * class's custom inspect method for its instances held as checked-dynamic + * values: the runtime's inspect walk calls it with the instance's typed + * ref and the method's three arguments. */ +function customInspectDynHelper( + lowerer: Lowerer, + t: IrType & { kind: "object" }, + loc: SrcLoc, +): string { + const name = keyedHelperName(lowerer, "%util.inspCustomDyn", typeKey(t)); + const found = customInspectMethodOf(lowerer, t.className)!; + const param = (id: string): IrExpr => varRef(id, DYN, loc); + const ids = ["depth.0", "options.0", "inspect.0"]; + const call = accessorCall( + lowerer, + t.className, + INSPECT_CUSTOM_MEMBER, + varRef("v.0", t, loc), + found.sig.params.map((p, i) => lowerer.coerceToExpected(param(ids[i]!), p.type)), + found.sig.ret, + loc, + ); + const body: IrStmt[] = [ + { kind: "varDecl", localId: "v.0", init: lowerer.coerceToExpected(param("self.0"), t), loc }, + ]; + if (found.sig.ret.kind === "void") { + body.push( + { kind: "exprStmt", expr: call, loc }, + { kind: "return", value: dynUndefinedExpr(loc), loc }, + ); + } else { + body.push({ kind: "return", value: lowerer.coerceToExpected(call, DYN), loc }); + } + const params = ["self.0", ...ids]; + lowerer.liftedFns.push({ + name, + params: params.map((id) => ({ localId: id, name: id.slice(0, -2), type: DYN })), + returnType: DYN, + locals: [ + ...params.map((id) => ({ id, name: id.slice(0, -2), type: DYN, mutable: false })), + { id: "v.0", name: "v", type: t, mutable: false }, + ], + body, + loc, + }); + return name; +} + +/** The statements %main runs first, in every thread: each class whose + * instances have a custom inspect method registers its compiled call + * under its instances' typed-ref type key (scr_insp_register_custom). */ +export function customInspectRegistrations(lowerer: Lowerer, loc: SrcLoc): IrStmt[] { + const stmts: IrStmt[] = []; + for (const info of lowerer.classes.values()) { + if (info.def.runtime || info.def.abstract) continue; + // Error classes register when their own properties render statically + // (fieldedErrorReason): the others keep no method to call. + if (isErrorClass(lowerer, info.def.name) && fieldedErrorReason(lowerer, info) !== null) + continue; + if (!lowerer.findMethodOn(info, INSPECT_CUSTOM_MEMBER)) continue; + // Only constructed classes have instances, and only their methods lower + // (the constructor edge reaches the method). + if (!lowerer.classCanBeConstructed(info)) continue; + if (customInspectReason(lowerer, info) !== null) continue; + const t = { kind: "object" as const, className: info.def.name }; + const fn = customInspectDynHelper(lowerer, t, loc); + stmts.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.registerCustom", + args: [ + strLit(typeKey(t), loc), + { + kind: "dynFrom", + value: { + kind: "closure", + fnName: fn, + captures: [], + type: funcOf([DYN, DYN, DYN, DYN], DYN), + loc, + }, + type: DYN, + loc, + }, + ], + type: VOID, + loc, + }, + loc, + }); + } + return stmts; +} + /* ── the interned per-type helper ────────────────────────────────────── */ /** `%util.insp.(v, r, d): string` — formatValue over the static type: @@ -2484,13 +2798,22 @@ function isDataViewTypeName(tname: string): boolean { /* ── the callsite: options, direct function/class names ──────────────── */ /** The parsed options literal: the resolved depth (numeric; Infinity for - * `null`), breakLength (numeric; default 80) and compact (numeric, 0 for + * `null`), breakLength (numeric; default 80) and compact (numeric, -1 for * false; default 3), with every other non-default knob fenced by name. */ interface InspectOptions { depth: number; breakLength: number; compact: number; sorted: boolean; + /** Set by the `atRunTime` parse: colors or a computed knob value, which + * the runtime reads. */ + runtime: boolean; + /** How the depth option was spelled, for custom inspect methods: 0 a + * number (also when absent), 1 null, 2 undefined. */ + depthKind: number; + /** customInspect: false skips [util.inspect.custom] methods (console.dir's + * default). */ + customInspect: boolean; } function parseInspectOptions( @@ -2498,9 +2821,18 @@ function parseInspectOptions( node: ts.Expression | undefined, recursive: boolean, dir = false, + atRunTime = false, ): InspectOptions { const surface = dir ? "console.dir" : "util.inspect"; - const options: InspectOptions = { depth: 2, breakLength: 80, compact: 3, sorted: false }; + const options: InspectOptions = { + depth: 2, + breakLength: 80, + compact: 3, + sorted: false, + runtime: false, + depthKind: 0, + customInspect: !dir, + }; if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return options; if (!ts.isObjectLiteralExpression(node)) { lowerer.noLowering( @@ -2511,6 +2843,11 @@ function parseInspectOptions( } for (const prop of node.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { + // `{ depth }` and `{ ...base }` hold values known at run time. + if (atRunTime && (ts.isShorthandPropertyAssignment(prop) || ts.isSpreadAssignment(prop))) { + options.runtime = true; + continue; + } lowerer.noLowering( `${surface} options in this form`, prop, @@ -2540,8 +2877,17 @@ function parseInspectOptions( // bounded by the (finite) object graph — exactly Node. void recursive; options.depth = Infinity; - } else if (!(ts.isIdentifier(value) && value.text === "undefined")) { + options.depthKind = value.kind === ts.SyntaxKind.NullKeyword ? 1 : 0; + } else if (ts.isIdentifier(value) && value.text === "undefined") { + // Node's ctx.depth is undefined: no depth limit. + options.depth = Infinity; + options.depthKind = 2; + } else { const n = numeric(); + if (n === null && atRunTime) { + options.runtime = true; + continue; + } if (n === null) { lowerer.noLowering( `${surface} with a non-literal depth option`, @@ -2552,7 +2898,8 @@ function parseInspectOptions( options.depth = n; } } else if (key === "colors") { - if (value.kind !== ts.SyntaxKind.FalseKeyword) { + if (value.kind !== ts.SyntaxKind.FalseKeyword && atRunTime) options.runtime = true; + else if (value.kind !== ts.SyntaxKind.FalseKeyword) { lowerer.noLowering( `${surface} with colors: true`, value, @@ -2564,6 +2911,10 @@ function parseInspectOptions( // compact of at least 1; false and smaller numbers break every // composite onto its own lines. compact: true is the older layout. const n = value.kind === ts.SyntaxKind.FalseKeyword ? 0 : numeric(); + if (n === null && atRunTime && value.kind !== ts.SyntaxKind.TrueKeyword) { + options.runtime = true; + continue; + } if (n === null || Number.isNaN(n)) { lowerer.noLowering( `${surface} with this compact option`, @@ -2571,9 +2922,22 @@ function parseInspectOptions( "compact must be false or a numeric literal (compact: true has no lowering)", ); } - options.compact = Math.max(n, 0); + // false is -1: the same layout as 0, and custom inspect functions + // read it back as false. + options.compact = value.kind === ts.SyntaxKind.FalseKeyword ? -1 : Math.max(n, 0); } else if (key === "sorted") { - if (value.kind !== ts.SyntaxKind.TrueKeyword && value.kind !== ts.SyntaxKind.FalseKeyword) { + const sortedLiteral = + value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword; + if ( + !sortedLiteral && + atRunTime && + !ts.isArrowFunction(value) && + !ts.isFunctionExpression(value) + ) { + options.runtime = true; + continue; + } + if (!sortedLiteral) { lowerer.noLowering( `${surface} with this sorted option`, value, @@ -2583,6 +2947,10 @@ function parseInspectOptions( options.sorted = value.kind === ts.SyntaxKind.TrueKeyword; } else if (key === "breakLength") { const n = ts.isIdentifier(value) && value.text === "Infinity" ? Infinity : numeric(); + if (n === null && atRunTime) { + options.runtime = true; + continue; + } if (n === null || Number.isNaN(n)) { lowerer.noLowering( `${surface} with a non-literal breakLength option`, @@ -2591,6 +2959,12 @@ function parseInspectOptions( ); } options.breakLength = n; + } else if ( + !dir && + key === "customInspect" && + (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) + ) { + options.customInspect = value.kind === ts.SyntaxKind.TrueKeyword; } else if (dir && key === "customInspect") { // console.dir's own default; only that value is lowered. if (value.kind !== ts.SyntaxKind.FalseKeyword) { @@ -2709,16 +3083,86 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo return null; } -/** util.inspect(value[, options]) — the spoke's inspect entry. */ +/** util.inspect(value[, options]) and inspect(value, showHidden, depth, + * colors) — the spoke's inspect entry. Options the compiler resolves + * render through the static helpers; colors, option values known only at + * run time and the legacy forms render the value's checked-dynamic view + * in the runtime (insp.inspect). */ function lowerInspectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { + const args = expr.arguments.nodes; + if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { lowerer.noLowering( - `util.inspect with ${expr.arguments.nodes.length} arguments`, + `util.inspect with ${args.some(ts.isSpreadElement) ? "spread" : args.length} arguments`, expr, - "the supported forms are inspect(value) and inspect(value, options)", + "the supported forms are inspect(value), inspect(value, options) and inspect(value, showHidden, depth, colors)", ); } - return lowerInspectValue(lowerer, expr.arguments.nodes[0]!, expr.arguments.nodes[1], loc, false); + if (args.length <= 2 && !inspectOptionsAtRunTime(lowerer, args[1])) + return lowerInspectValue(lowerer, args[0]!, args[1], loc, false); + return lowerRuntimeInspect(lowerer, args, loc); +} + +/** Whether an options argument needs the runtime reader: an options value + * that is not an object literal, or a literal with colors or a computed + * depth, compact, breakLength or sorted value. Literal options otherwise + * keep the static parse, which also reports the literal options neither + * path lowers. */ +function inspectOptionsAtRunTime(lowerer: Lowerer, node: ts.Expression | undefined): boolean { + if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return false; + if (!ts.isObjectLiteralExpression(node)) return true; + return parseInspectOptions(lowerer, node, false, false, true).runtime; +} + +/** util.inspect over the value's checked-dynamic view, with every argument + * evaluated in order and read in the runtime as Node's inspect reads it. */ +function lowerRuntimeInspect( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr { + const valueNode = args[0]!; + if ( + directCallableInspect(lowerer, valueNode, loc) !== null || + ts.isArrowFunction(valueNode) || + ts.isFunctionExpression(valueNode) + ) { + lowerer.noLowering( + "util.inspect of a function or class with these options", + valueNode, + "pass the options as a literal without colors", + ); + } + const value = lowerer.lowerExpr(valueNode); + if (value.type.kind !== "dyn") { + const reason = inspectSupport(lowerer, value.type, new Set(), { recursive: false }); + if (reason !== null) + lowerer.noLowering(`util.inspect of '${lowerer.fmt(value.type)}' values`, valueNode, reason); + if (!lowerer.dynConvertible(value.type)) + lowerer.noLowering( + `util.inspect with these options of '${lowerer.fmt(value.type)}' values`, + valueNode, + "the options are read at run time, where the value renders through its checked-dynamic view", + ); + } + const stmts: IrStmt[] = []; + const held = (init: IrExpr): IrExpr => { + const local = lowerer.declareHiddenLocal("%inspectArg", DYN); + stmts.push({ kind: "varDecl", localId: local.id, init, loc }); + return varRef(local.id, DYN, loc); + }; + const operands: IrExpr[] = [held(lowerer.coerceToExpected(value, DYN))]; + for (let i = 1; i < 4; i++) { + const node = args[i]; + operands.push(node ? held(lowerer.lowerExprExpecting(node, DYN)) : dynUndefinedExpr(loc)); + } + operands.push(numLit(args.length, loc)); + return { + kind: "seqExpr", + stmts, + result: { kind: "libCall", fn: "insp.inspect", args: operands, type: STRING, loc }, + type: STRING, + loc, + }; } /** `inspect(value, options)` over one value node: the shared body of @@ -2818,6 +3262,47 @@ export function lowerInspectValue( } const options = parseInspectOptions(lowerer, optionsNode, walk.recursive, dir); const depth = options.depth; + // Custom inspect functions read the options as Node spelled them: an + // unbounded depth given as Infinity or undefined, and customInspect: + // false (console.dir's default), apply around the render when the value + // can reach a custom method; the previous settings return afterwards. + const reachesCustom = + typeReachesCustomInspect(lowerer, value.type) || typeReachesDyn(lowerer, value.type); + const depthContext = reachesCustom && depth === Infinity && options.depthKind !== 1; + const customContext = reachesCustom && !options.customInspect; + if (depthContext || customContext) { + const stmts: IrStmt[] = []; + const restore: IrStmt[] = []; + const setting = (fn: "insp.customInspect" | "insp.depthKind", arg: IrExpr, type: IrType) => { + const previous = lowerer.declareHiddenLocal("%inspectContext", type); + const call = (a: IrExpr): IrExpr => ({ kind: "libCall", fn, args: [a], type, loc }); + stmts.push({ kind: "varDecl", localId: previous.id, init: call(arg), loc }); + restore.push({ kind: "exprStmt", expr: call(varRef(previous.id, type, loc)), loc }); + }; + if (customContext) setting("insp.customInspect", boolLit(false, loc), BOOL); + if (depthContext) setting("insp.depthKind", numLit(options.depthKind, loc), F64); + const text = lowerer.declareHiddenLocal("%inspectText", STRING); + stmts.push({ + kind: "varDecl", + localId: text.id, + init: withInspectLayout( + lowerer, + options, + value, + "inspect", + (input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc), + loc, + ), + loc, + }); + return { + kind: "seqExpr", + stmts: [...stmts, ...restore.reverse()], + result: varRef(text.id, STRING, loc), + type: STRING, + loc, + }; + } if (dir && value.type.kind === "dyn") { return withInspectLayout( lowerer, @@ -2850,7 +3335,17 @@ export function lowerInspectValue( * formatNumber, strings verbatim, objects through inspect at the given * depth). Null = fence with the given reason. */ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: SrcLoc): IrExpr { - const value = lowerer.lowerExpr(node); + return formatSValue(lowerer, node, lowerer.lowerExpr(node), depth, loc); +} + +/** formatSArg over the already-lowered value of `node`. */ +function formatSValue( + lowerer: Lowerer, + node: ts.Expression, + value: IrExpr, + depth: number, + loc: SrcLoc, +): IrExpr { const t = value.type; if (t.kind === "string") return value; if (t.kind === "f64") @@ -2867,7 +3362,7 @@ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S if (t.kind === "dyn") { return { kind: "libCall", - fn: "insp.dynS", + fn: "insp.dynFormatS", args: [value, numLit(depth, loc)], type: STRING, loc, @@ -2945,6 +3440,13 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S node, reason, ); + if (depth === 4 && typeReachesCustomInspect(lowerer, value.type)) { + lowerer.noLowering( + "util.format %o over values with [util.inspect.custom] methods", + node, + "%o passes showHidden to the methods — use %O", + ); + } if (depth === 4 && typeTreeHasHidden(lowerer, value.type, new Set())) { lowerer.noLowering( "util.format %o over arrays", @@ -3027,8 +3529,9 @@ function formatIntegerFixed(value: IrExpr, spec: "%d" | "%i", loc: SrcLoc): IrEx /** util.format(...) / util.formatWithOptions({}, ...): the compile-time * %-substitution over a literal format string (formatWithOptionsInternal * ported exactly — the arg cursor, the args-exhausted guard, %c's - * consume-and-drop), with per-static-type conversions. Non-literal - * format strings lower only in the substitution-free shapes. */ + * consume-and-drop), with per-static-type conversions. Format strings + * known only at run time, spread arguments and non-empty options lower to + * the runtime formatter (lowerRuntimeFormat, heldFormatArgs). */ export function lowerFormatCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -3040,30 +3543,61 @@ export function lowerFormatCall( const argNodes = [...(argNodesOverride ?? expr.arguments.nodes)]; if (withOptions) { const opts = argNodes.shift(); + // The empty literal {} is the defaults: exactly util.format. if (!opts || !ts.isObjectLiteralExpression(opts) || opts.properties.nodes.length > 0) { - lowerer.noLowering( - "util.formatWithOptions with a non-empty options literal", - opts ?? expr, - "only the empty literal {} (the defaults — exactly util.format) is lowered", - ); + if (opts && ts.isSpreadElement(opts)) { + lowerer.noLowering( + "util.formatWithOptions with a spread options argument", + opts, + "pass the options object as the first argument", + ); + } + return lowerRuntimeFormat(lowerer, argNodes, loc, opts ?? null); } } if (argNodes.length === 0) return strLit("", loc); const first = argNodes[0]!; const firstIsLiteral = ts.isStringLiteral(first) || ts.isNoSubstitutionTemplateLiteral(first); + if (argNodes.some(ts.isSpreadElement)) { + if (literalPrefix !== "" && !firstIsLiteral) + throw new InternalCompilerError("a format prefix needs a literal format string"); + return lowerRuntimeFormat(lowerer, argNodes, loc, undefined, literalPrefix); + } /** The rest-args tail: ` ` + (strings verbatim, everything else * inspected at the default depth). */ const restArg = (node: ts.Expression): IrExpr => formatSArg(lowerer, node, 2, loc); if (!firstIsLiteral) { - const firstT = lowerer.mapTypeOf(lowerer.typeOf(first)); - if (firstT?.kind === "string" && argNodes.length > 1) { - lowerer.noLowering( - "util.format with a runtime format string and further arguments", - first, - "the %-substitution positions are runtime-dependent — use a string literal", - ); + const runtime = argNodes.length > 1 ? runtimeFormatString(lowerer, first) : null; + if (runtime === "other" && !formatStringConverts(lowerer, first)) + return lowerRuntimeFormat(lowerer, argNodes, loc); + if (runtime !== null) { + // A string that substitutes formats at run time; any other string + // joins with the remaining arguments, as below. + const held = heldFormatArgs(lowerer, argNodes, loc); + if (!held.joinable) { + return { kind: "seqExpr", stmts: held.stmts, result: held.format(), type: STRING, loc }; + } + const joined: IrExpr[] = []; + held.args.forEach((arg, i) => { + if (i > 0) joined.push(strLit(" ", loc)); + joined.push(formatRestValue(lowerer, arg.node, arg.ref(), loc)); + }); + return { + kind: "seqExpr", + stmts: held.stmts, + result: { + kind: "ternary", + cond: held.specifies, + then: held.format(), + else_: concatAll(joined, loc), + type: STRING, + loc, + }, + type: STRING, + loc, + }; } // No substitutions possible: every argument joins with spaces. const parts: IrExpr[] = []; @@ -3349,6 +3883,257 @@ export function lowerFormatCall( return concatAll(parts, loc); } +/** One space-joined argument of util.format, as Node's formatter renders + * it after the format string: strings verbatim, everything else through + * inspect at the default depth. */ +function formatRestValue( + lowerer: Lowerer, + node: ts.Expression, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + switch (value.type.kind) { + case "string": + return value; + case "f64": + return { kind: "libCall", fn: "insp.f64", args: [value], type: STRING, loc }; + case "bool": + return { kind: "toString", operand: value, type: STRING, loc }; + case "dyn": + return { kind: "libCall", fn: "insp.dynS", args: [value, numLit(2, loc)], type: STRING, loc }; + default: + return lowerConsoleInspectArg(lowerer, node, value, "util.format", loc); + } +} + +const UNCONVERTIBLE_HINT = + "the runtime formatter reads its arguments as checked-dynamic values — use a string literal format"; + +/** A string-valued expression that throws the SC2020 refusal where it runs + * (a JavaScript construct fence deferred to the branch that needs it). */ +function runtimeFenceValue( + lowerer: Lowerer, + surface: string, + node: ts.Node, + hint: string, + loc: SrcLoc, +): IrExpr { + const diag = noLoweringDiag(surface, locOf(node), hint); + lowerer.runtimeFences.push(diag); + const sf = ts.getSourceFileOfNode(node); + const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; + const slot = lowerer.declareHiddenLocal("%never", STRING); + slot.mutable = true; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: null, loc }, + { + kind: "runtimeFence", + code: diag.code, + message: `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`, + loc, + }, + ], + result: varRef(slot.id, STRING, loc), + diverges: true, + type: STRING, + loc, + }; +} + +/** Whether the held-argument form can test a format string whose type + * only may hold a string: a union that converts to a checked-dynamic value, + * or a value whose checker type does not map (its lowered type decides). */ +export function formatStringConverts(lowerer: Lowerer, first: ts.Expression): boolean { + const t = lowerer.mapTypeOf(lowerer.typeOf(first)); + return t === null || t.kind === "dyn" || lowerer.dynConvertible(t); +} + +/** How a non-literal format string is known: "string" and "dyn" values + * test for a substitution at run time, "other" types (unions with a string + * arm) format at run time, and null types never hold a string. */ +export function runtimeFormatString( + lowerer: Lowerer, + first: ts.Expression, +): "string" | "dyn" | "other" | null { + const t = lowerer.mapTypeOf(lowerer.typeOf(first)); + if (t === null) return "other"; + if (t.kind === "string") return "string"; + if (t.kind === "dyn") return "dyn"; + if (t.kind === "union") { + const arms = lowerer.unions.get(t.unionId)?.arms ?? []; + return arms.some((arm) => arm.kind === "string" || arm.kind === "dyn") ? "other" : null; + } + return t.kind === "jsval" || t.kind === "caught" ? "other" : null; +} + +/** The runtime formatter over a call's arguments: one checked-dynamic + * pack in source order (spreads contribute their elements), after the + * options when `options` is given (null: an omitted options argument, + * which Node's validation rejects). A literal prefix joins a literal + * format string. */ +export function lowerRuntimeFormat( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, + options?: ts.Expression | null, + literalPrefix = "", +): IrExpr { + const stmts: IrStmt[] = []; + let optionsRef: IrExpr | null = null; + if (options !== undefined) { + const held = lowerer.declareHiddenLocal("%formatOptions", DYN); + stmts.push({ + kind: "varDecl", + localId: held.id, + init: options === null ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(options, DYN), + loc, + }); + optionsRef = varRef(held.id, DYN, loc); + } + let nodes = argNodes; + const leading: IrExpr[] = []; + if (literalPrefix !== "") { + const first = argNodes[0] as ts.StringLiteralLike; + leading.push(strLit(literalPrefix + first.text, loc)); + nodes = argNodes.slice(1); + } + const pack = dynArgumentPack(lowerer, nodes, loc, { + leading, + blame: argNodes[0] ?? options ?? undefined, + }); + stmts.push(...pack.stmts); + const result: IrExpr = + optionsRef === null + ? { kind: "libCall", fn: "insp.format", args: [pack.pack()], type: STRING, loc } + : { + kind: "libCall", + fn: "insp.formatWithOptions", + args: [optionsRef, pack.pack()], + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; +} + +/** The arguments of a call whose first argument is a string or dyn value + * known only at run time, each evaluated once into a hidden local: + * `specifies` tests that string for a '%' before its last character, and + * `format()` is the runtime formatter over the held values. The caller + * renders the other branch (the space-joined arguments) from `args` when + * every argument is `joinable` there. */ +export function heldFormatArgs( + lowerer: Lowerer, + argNodes: readonly ts.Expression[], + loc: SrcLoc, +): { + stmts: IrStmt[]; + args: { node: ts.Expression; ref: () => IrExpr }[]; + specifies: IrExpr; + format: () => IrExpr; + joinable: boolean; +} { + const stmts: IrStmt[] = []; + let unconvertible: { node: ts.Expression; type: IrType } | null = null; + const unconvertibleSurface = (t: IrType): string => + `util.format with a runtime format string over '${lowerer.fmt(t)}' values`; + const args = argNodes.map((node) => { + const value = lowerer.lowerExpr(node); + if (value.type.kind === "void") { + lowerer.unsupported( + "SC1090", + node, + "util.format of a void call result (call it as its own statement, then pass undefined)", + ); + } + // The space-joined branch prints these values as before; only a string + // that substitutes refuses them, where it runs. + if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) + unconvertible ??= { node, type: value.type }; + // Unit values have no local: their effects run here and a literal + // stands in. + const k = value.type.kind; + if (k === "undefinedT" || k === "nullT") { + if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc }); + const unit = k === "nullT" ? ("null" as const) : ("undefined" as const); + return { node, ref: (): IrExpr => ({ kind: "unitLit", unit, type: value.type, loc }) }; + } + const held = lowerer.declareHiddenLocal("%formatArg", value.type); + stmts.push({ kind: "varDecl", localId: held.id, init: value, loc }); + return { node, ref: () => varRef(held.id, value.type, loc) }; + }); + const first = args[0]!.ref(); + // A lowered value that is neither a string nor convertible holds no + // string, so it never substitutes. + const specifies: IrExpr = + first.type.kind === "string" + ? { kind: "libCall", fn: "insp.formatSpecifies", args: [first], type: BOOL, loc } + : first.type.kind === "dyn" || lowerer.dynConvertible(first.type) + ? { + kind: "libCall", + fn: "insp.formatSpecifiesDyn", + args: [lowerer.coerceToExpected(first, DYN)], + type: BOOL, + loc, + } + : { kind: "boolLit", value: false, type: BOOL, loc }; + const format = (): IrExpr => { + if (unconvertible !== null) { + const fenced = unconvertible; + return runtimeFenceValue( + lowerer, + unconvertibleSurface(fenced.type), + fenced.node, + UNCONVERTIBLE_HINT, + loc, + ); + } + // Strings, numbers, booleans, null and undefined format without the + // checked-dynamic walk other values need, which keeps it out of the + // executable. + const primitive = (t: IrType): boolean => + t.kind === "string" || + t.kind === "f64" || + t.kind === "bool" || + t.kind === "undefinedT" || + t.kind === "nullT" || + (t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms.every(primitive) ?? false)); + // A native list of the checked values: converting into it is a tier + // boundary, so the statement stays static. + return { + kind: "libCall", + fn: args.every((arg) => primitive(arg.ref().type)) + ? "insp.formatPrimitives" + : "insp.formatList", + args: [ + { + kind: "arrayLit", + elems: args.map((arg) => lowerer.coerceToExpected(arg.ref(), DYN)), + type: arrayOf(DYN), + loc, + }, + ], + type: STRING, + loc, + }; + }; + // The space-joined branch renders each argument statically; arguments + // the static rendering refuses take the runtime formatter in both cases. + const joinable = args.every(({ ref }) => { + const t = ref().type; + if (t.kind === "func") return false; + if (t.kind === "bytes") return true; + return inspectSupport(lowerer, t, new Set(), { recursive: false }) === null; + }); + // Without a joined branch every call formats: TypeScript refuses the + // value at compile time. + const refused = unconvertible as { node: ts.Expression; type: IrType } | null; + if (!joinable && refused !== null && !isJsSourceFile(ts.getSourceFileOfNode(refused.node))) + lowerer.noLowering(unconvertibleSurface(refused.type), refused.node, UNCONVERTIBLE_HINT); + return { stmts, args, specifies, format, joinable }; +} + /** Arrays anywhere in the tree — the %o (showHidden) gate. */ function typeTreeHasHidden(lowerer: Lowerer, t: IrType, visiting: Set): boolean { switch (t.kind) { diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 4fe908ab4..3f4a70c74 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -97,6 +97,7 @@ import { createRequireProgramModuleDecl, } from "./builtins/module-bindings.js"; import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; +import { customInspectRegistrations } from "./lower-inspect.js"; import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; import { isWebCryptoMemberType } from "./builtins/webcrypto.js"; @@ -1973,18 +1974,21 @@ export function collectGlobals( // symbol registers HERE, before any use site can lower, and the // statement lowering skips the declaration by the same check. A // bad promisify target reports here (recorded once; the statement - // lowering poisons too, like every collectGlobals failure). + // lowering poisons too, like every collectGlobals failure). Targets + // that promisify to a function value (typed callback-last functions + // and untyped functions) keep an ordinary global. if ( ts.isIdentifier(decl.name) && decl.initializer && isPromisifyCall(lowerer, decl.initializer) ) { + let registered = true; try { - lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer); + registered = lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer); } catch (e) { if (!(e instanceof PoisonError)) throw e; } - continue; + if (registered) continue; } // `const inspect = require("util").inspect` at file scope: a // named import in const clothing (builtinImportOf resolves uses) @@ -3549,6 +3553,9 @@ export function buildMain(lowerer: Lowerer): IrFunction { loc, }); } + // Classes with custom inspect methods register their compiled calls for + // the runtime's inspect walk before any module runs, in every thread. + body.unshift(...customInspectRegistrations(lowerer, loc)); // Node's startup refusal (a resolution the graph carries that Node // rejects — preflight's Node-order resolution walk — or a module-LINK // SyntaxError from either named-import checker): the graph is refused diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 5635fd4d3..56f899525 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -69,6 +69,7 @@ import { isDiffieHellmanType, lowerDiffieHellmanCall } from "./builtins/dh.js"; import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; +import { lowerMimeCall, lowerMimeNew } from "./builtins/mime.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { iteratorSlotAdapter } from "./iterator-adapters.js"; import { iterableSlotValue } from "./iterable-values.js"; @@ -177,7 +178,7 @@ import { import { DeferredModuleInitializers } from "./deferred-module-initializers.js"; import { finalizeClassMethodValues } from "./class-method-values.js"; import { finalizePrivateBrands } from "./private-brands.js"; -import { type ClassSymbolKey, tableKeyFieldType } from "./symbol-fields.js"; +import { type ClassSymbolKey, INSPECT_CUSTOM_MEMBER, tableKeyFieldType } from "./symbol-fields.js"; import type { HttpClientFnBinding } from "./lower-server.js"; export type { WidthLift } from "./width-lift.js"; export { newFnCtx, type FnCtx } from "./function-context.js"; @@ -8689,6 +8690,19 @@ export class Lowerer { this.constructedClasses.add(name.slice(1, -".constructor".length)); for (const [owner, methods] of this.virtualEdges) for (const method of methods) this.noteVirtualEdge(owner, method); + // An instance's [util.inspect.custom] method runs from the runtime + // inspect walk (customInspectRegistrations), which no IR edge shows. + for ( + let c = this.classes.get(name.slice(1, -".constructor".length)) ?? null; + c; + c = c.base + ) { + const custom = `%${c.def.name}.${INSPECT_CUSTOM_MEMBER}`; + if (units.has(custom)) { + this.onEdge?.(custom); + break; + } + } } }; // Class EXPRESSIONS collect while init bodies lower (below): their @@ -13850,6 +13864,7 @@ export class Lowerer { lowerEcdhCall(this, expr) ?? lowerDiffieHellmanCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? + lowerMimeCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? lowerDynamicImportCall(this, expr) ?? lowerCall(this, expr) @@ -14087,6 +14102,7 @@ export class Lowerer { lowerResponseNew(this, expr) ?? lowerBlobNew(this, expr) ?? lowerNetAddressNew(this, expr) ?? + lowerMimeNew(this, expr) ?? lowerStaticReadableStreamNew(this, expr) ?? lowerStaticTransformStreamNew(this, expr) ?? lowerConsoleNew(this, expr); @@ -14531,6 +14547,10 @@ export class Lowerer { const assertion = assertFunctionValue(bi, loc); if (assertion) return assertion; if (bi.module === "util" && bi.member === "types") return utilTypesModuleValue(this, loc); + // util.inspect: the function custom inspect methods receive, one + // identity per thread. + if (bi.module === "util" && bi.member === "inspect") + return { kind: "libCall", fn: "insp.inspectFunction", args: [], type: DYN, loc }; if (bi.module === "util/types" && builtinModuleFnOf(this, bi.module, bi.member)) return lowerUtilTypeValue(this, bi.member, loc); if (bi.module === "util" && bi.member === "getSystemErrorMap") diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index b5051d79d..cf0428b60 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1740,7 +1740,8 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable { // lowerVarDecl / collectGlobals BEFORE any call lowering runs — and // inspect/format/formatWithOptions plus parseArgs through the util spoke // (lower-inspect.ts: per-type synthesized traversal helpers, - // compile-time format strings, checked-dynamic config/result for + // compile-time format strings and the runtime formatter for the others, + // checked-dynamic config/result for // parseArgs), which both dispatch paths try before the generic table. // parseArgs is tabled as well so the generated surface manifest records // the dedicated static member; the spoke owns its optional arity and @@ -1767,6 +1768,67 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable { ], }, parseArgs: { fn: "util.parseArgs", params: [DYN], result: DYN }, + aborted: { + fn: "util.aborted", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + diff: { + fn: "util.diff", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + debounce: { + fn: "util.debounce", + params: [DYN, DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + throttle: { + fn: "util.throttle", + params: [DYN, DYN, DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + convertProcessSignalToExitCode: { + fn: "util.signalExitCode", + params: [DYN], + result: F64, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + // The deprecated helpers report DEP0044 and DEP0060 on their first call. + isArray: { + fn: "util.isArrayDeprecated", + params: [DYN], + result: BOOL, + valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }], + }, + _extend: { + fn: "util.extend", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, parseEnv: { fn: "util.parseEnv", params: [DYN], result: DYN }, getSystemErrorName: { fn: "util.getSystemErrorName", diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index 958af5963..d017cdc7f 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -76,7 +76,36 @@ export function fenceSymbolFieldCopy( } } +/** The registry name util.inspect.custom denotes. */ +export const INSPECT_CUSTOM_DESCRIPTION = "nodejs.util.inspect.custom"; +/** The method slot of a class's `[util.inspect.custom]` member. */ +export const INSPECT_CUSTOM_MEMBER = `${FIELD_PREFIX}Symbol(${INSPECT_CUSTOM_DESCRIPTION})`; + +/** `inspect.custom` over node:util's inspect: a named import, `util.inspect` + * or the CommonJS `util_1.inspect`. */ +export function isInspectCustomAccess(lowerer: Lowerer, expr: ts.Expression): boolean { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isPropertyAccessExpression(expr) || expr.questionDotToken) return false; + if (!ts.isIdentifier(expr.name) || expr.name.text !== "custom") return false; + let target = expr.expression; + while (ts.isParenthesizedExpression(target)) target = target.expression; + const bi = ts.isIdentifier(target) + ? lowerer.builtinImportOf(target) + : ts.isPropertyAccessExpression(target) && !target.questionDotToken + ? lowerer.builtinMemberOf(target) + : null; + return bi?.module === "util" && bi.member === "inspect"; +} + export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null { + // util.inspect.custom is the registered Symbol.for(INSPECT_CUSTOM_DESCRIPTION), + // one identity with the program's own Symbol.for calls of that name. + if (isInspectCustomAccess(lowerer, key)) { + let e = key; + while (ts.isParenthesizedExpression(e)) e = e.expression; + const sym = lowerer.checker.getSymbolAtLocation((e as ts.PropertyAccessExpression).name); + return sym ? symbolKeyOf(lowerer, sym, true, INSPECT_CUSTOM_DESCRIPTION) : null; + } const wellKnown = wellKnownSymbolKeyOf(lowerer, key); if (wellKnown !== undefined) return wellKnown; if ( diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 832857448..4b1bf2a42 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -10,6 +10,7 @@ import { isDynTypedRefType, BOOL, bytesOf, + canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, CHILD_T, @@ -717,6 +718,12 @@ export class UnionRegistry { return id !== undefined && this.pendingRec.has(id); } + /** True when `unionId` is a recursive placeholder whose arms are not + * known yet (its outer frame is still mapping, or failed). */ + isPendingPlaceholder(unionId: string): boolean { + return this.pendingRec.has(unionId); + } + /** Completes a recursive placeholder with its canonical arm list and * registers the structural key (first writer wins, like shapes). */ finalizeRecursive(t: ts.Type, arms: IrType[], discriminant?: IrUnionDiscriminant): string { @@ -3029,6 +3036,30 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // spawn consumes a presence-sensitive option bag, including optional // stdio and env values. Keep it checked-dynamic for native normalization. if (isStdlibInterface("SpawnOptions")) return DYN; + // util.inspect as a value (a custom inspect method's third parameter is + // `typeof inspect`): the runtime's inspect function, called through the + // checked-dynamic boundary. + if ( + psym !== undefined && + ts.symbolName(psym) === "inspect" && + psym.declarations.some( + (d) => + ts.isFunctionDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientModule(d, "util"), + ) + ) + return DYN; + // util.inspect's options bag, which inspect and formatWithOptions read + // at run time (its sorted option can be a comparator function), and the + // options custom inspect functions receive. + if ( + (isStdlibInterface("InspectOptions") || isStdlibInterface("InspectOptionsStylized")) && + psym!.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientModule(d, "util"), + ) + ) + return DYN; // dns.LookupAllOptions (`all: true`, the overload whose callback takes // an address list): a lookup option bag dns.lookup reads at run time. // It rides checked-dynamic storage, so the lookup function type that @@ -3092,11 +3123,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (!elem) return null; return genResultRecord(elem, VOID, ctx.shapes, ctx.unions); } - // node:net's SocketAddress and BlockList: native handles (scr_net.c) - // whose getters and methods dispatch through the handle. + // node:net's SocketAddress and BlockList (scr_net.c) and node:util's + // MIMEType and MIMEParams (scr_mime.c): native handles whose getters and + // methods dispatch through the handle. if ( ((psym !== undefined && ts.symbolName(psym) === "SocketAddress") || - (psym !== undefined && ts.symbolName(psym) === "BlockList")) && + (psym !== undefined && ts.symbolName(psym) === "BlockList") || + (psym !== undefined && ts.symbolName(psym) === "MIMEType") || + (psym !== undefined && ts.symbolName(psym) === "MIMEParams")) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && @@ -3105,6 +3139,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // The functions node:util's debounce and throttle return + // (scr_util_timing.c): native functions with their own members and + // getters. + if ( + psym !== undefined && + (ts.symbolName(psym) === "DebouncedFunction" || ts.symbolName(psym) === "ThrottledFunction") && + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) + ) { + return DYN; + } // WebCrypto: the crypto global and its subtle object are dynamic // objects, CryptoKey values are native handles (scr_webcrypto.c), and // the algorithm dictionaries the methods take and keys report are plain @@ -5335,8 +5381,19 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); // Belt and braces for non-strict type worlds: an optional param's // ABI slot is always the undefined-armed union (strictNullChecks - // already spells it that way; arm it here if the world didn't). - if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") { + // already spells it that way; arm it here if the world didn't). A + // back-reference to a union still being mapped (`options?: { then?: + // (ctx: Sub) => void }` where Sub extends the interface declaring the + // method) is a recursive placeholder with no arms yet; when the checker + // spelled the parameter `X | undefined`, the placeholder receives that + // undefined arm when its frame finishes. + const pendingOptional = + pt?.kind === "union" && + ctx.unions.isPendingPlaceholder(pt.unionId) && + ts + .constituentTypes(checker.getTypeOfSymbol(p)) + .some((part) => (part.flags & ts.TypeFlags.Undefined) !== 0); + if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn" && !pendingOptional) { const armed = withUndefinedArm(pt, ctx.unions); if (!armed) return null; pt = armed; @@ -8398,6 +8455,16 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { // `string | URL | Request`, fetch's own input type under @types/node. case "url": return true; + // A function the checked-dynamic tree can box (each parameter checks + // in, the result converts out) is held by reference there, and + // `typeof x === "function"` narrows it back out: `Progress | AbortSignal` + // beside fetch's handles. + case "func": + return canBoxFuncIntoDyn( + arm, + (id) => ctx.shapes.get(id), + (id) => ctx.unions.get(id), + ); case "record": case "array": // `T | Promise` with T bound to `unknown`: the checked-dynamic value diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 11b0f6d13..c58edcdca 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -650,6 +650,8 @@ async function compileExecutableNative( ...(features.declAdopt ? { declAdopt: true } : {}), ...(features.dynForAwait ? { dynForAwait: true } : {}), ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.mime ? { mime: true } : {}), + ...(features.utilTiming ? { utilTiming: true } : {}), ...(features.dynBuffer ? { dynBuffer: true } : {}), ...(features.builtinValues ? { builtinValues: true } : {}), events: features.events, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 72621f10c..b0cc96687 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -185,6 +185,16 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "num.toStringRadix", "num.toStringRadixF64", "insp.jsonDyn", + // The runtime formatter calls toString, valueOf and toJSON, and refuses + // renderings it has no lowering for. + "insp.format", + "insp.formatList", + "insp.formatWithOptions", + "insp.inspect", + // The options object reads the caller's options (getters included), and + // the checked-dynamic walk calls custom inspect functions. + "insp.customOptions", + "insp.customResult", // A checked-dynamic value the inspector does not render (a promise, a // URL, a RegExp) throws the 'not supported yet' Error. "insp.dyn", @@ -599,6 +609,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "util.inherits", "util.inheritsPrototype", "util.deprecate", + "util.extend", + "util.signalExitCode", + "util.diff", + "util.debounce", + "util.throttle", + "mime.typeNew", + "mime.typeParse", "util.callbackify", "util.promisifyDyn", "util.debuglog", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 091504e65..e230932ba 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1156,6 +1156,17 @@ function buildLibFnSignatures() { "util.inherits": { argTypes: [DYN, DYN], result: VOID }, "util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID }, "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "util.isArrayDeprecated": { argTypes: [DYN], result: BOOL }, + "util.extend": { argTypes: [DYN, DYN], result: DYN }, + "util.signalExitCode": { argTypes: [DYN], result: F64 }, + "util.aborted": { argTypes: [DYN, DYN], result: DYN }, + "util.diff": { argTypes: [DYN, DYN], result: DYN }, + "util.debounce": { argTypes: [DYN, DYN, DYN], result: DYN }, + "util.throttle": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "mime.typeNew": { argTypes: [DYN, DYN], result: DYN }, + "mime.typeParse": { argTypes: [DYN], result: DYN }, + "mime.paramsNew": { argTypes: [], result: DYN }, + "mime.is": { argTypes: [DYN, F64], result: BOOL }, "util.callbackify": { argTypes: [DYN], result: DYN }, /** util.promisify over an untyped function value: a native dyn function * whose calls answer a dyn promise the appended callback settles. */ @@ -4600,12 +4611,54 @@ function buildLibFnSignatures() { "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, "insp.dynS": { argTypes: [DYN, F64], result: STRING }, + /** util.format over a dyn array of its arguments, and + * util.formatWithOptions(options, pack): the runtime formatter for format + * strings known at run time and spread arguments. */ + "insp.format": { argTypes: [DYN], result: STRING }, + /** insp.format over arguments that are all strings, numbers, booleans, + * null or undefined, without the checked-dynamic walk. */ + "insp.formatPrimitives": { argTypes: [arrayOf(DYN)], result: STRING }, + "insp.formatList": { argTypes: [arrayOf(DYN)], result: STRING }, + "insp.formatWithOptions": { argTypes: [DYN, DYN], result: STRING }, + /** util.inspect(value, options, depth, colors) with the call's argument + * count: the runtime reads the options and renders the checked-dynamic + * view. */ + "insp.inspect": { argTypes: [DYN, DYN, DYN, DYN, F64], result: STRING }, + /** A [util.inspect.custom] method's call: whether ctx.customInspect is + * set, its depth argument (the remaining depth at recursion r of depth + * d), its options object and util.inspect, then its result rendered: a + * string re-indented at the current level, or a checked-dynamic value + * (customIsSelf tests for the instance itself). customInspect sets the + * flag for the renders that follow and answers the previous value, and + * depthKind does the same for how an unbounded depth was given (0 + * Infinity, 1 null, 2 undefined). */ + "insp.customEnabled": { argTypes: [], result: BOOL }, + /** Registers a class's compiled custom inspect function under its + * instances' typed-ref type key, for the checked-dynamic walk. */ + "insp.registerCustom": { argTypes: [STRING, DYN], result: VOID }, + "insp.customInspect": { argTypes: [BOOL], result: BOOL }, + "insp.depthKind": { argTypes: [F64], result: F64 }, + "insp.customDepth": { argTypes: [F64, F64], result: DYN }, + "insp.customOptions": { argTypes: [F64], result: DYN }, + "insp.inspectFunction": { argTypes: [], result: DYN }, + "insp.customText": { argTypes: [STRING], result: STRING }, + "insp.customIsSelf": { argTypes: [DYN, DYN], result: BOOL }, + "insp.customResult": { argTypes: [DYN, F64, F64], result: STRING }, + /** Around the method's call: the inspect and format calls it makes run + * in a fresh inspect context with the default options. begin answers the + * token end takes; end also discards what a call that threw left. */ + "insp.customCallBegin": { argTypes: [], result: F64 }, + "insp.customCallEnd": { argTypes: [F64], result: VOID }, + /** Whether a runtime format string has a '%' before its last character. */ + "insp.formatSpecifies": { argTypes: [STRING], result: BOOL }, + "insp.formatSpecifiesDyn": { argTypes: [DYN], result: BOOL }, + "insp.dynFormatS": { argTypes: [DYN, F64], result: STRING }, /** util.format's %o over a checked-dynamic value: inspect with * showHidden. Kinds whose hidden entries have no rendering throw the * 'not supported yet' Error. */ "insp.dynHidden": { argTypes: [DYN, F64], result: STRING }, "insp.begin": { argTypes: [F64], result: VOID }, - /** util.inspect's breakLength, compact (0 for false) and sorted for the + /** util.inspect's breakLength, compact (-1 for false) and sorted for the * renders that follow; the call site restores 80, 3 and false afterwards. */ "insp.layout": { argTypes: [F64, F64, BOOL], result: VOID }, "insp.entry": { argTypes: [STRING, BOOL], result: VOID }, diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 79702ce97..fbd51dbc5 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -575,6 +575,19 @@ const DYN_READ_ONLY = new Set([ "buffer.poolFromDynRange", "insp.dyn", "insp.dynS", + // util.format and util.inspect over checked values: the inspect walk and + // the format conversions read them; insp.registerCustom keeps a class's + // custom inspect thunk in the runtime's registry. + "insp.customIsSelf", + "insp.customResult", + "insp.format", + "insp.formatList", + "insp.formatPrimitives", + "insp.formatSpecifiesDyn", + "insp.formatWithOptions", + "insp.inspect", + "insp.registerCustom", + "insp.dynFormatS", "insp.dynHidden", // The dynamic-tier operators run their dyn twins (scr_js_value.c); the // conversions move a value between dyn and jsval, and js.toF64 is diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts index ea866a134..427f6879c 100644 --- a/packages/compiler/src/ir/integer-loops.ts +++ b/packages/compiler/src/ir/integer-loops.ts @@ -618,6 +618,28 @@ function writesLocal(body: IrStmt[], localId: string): boolean { }); } +/** The counter of a `for` loop that its condition reads and its update + * steps by a constant, when nothing else in the loop writes it: each test + * of the condition then sees a different value. The local must not be + * boxed (a closure could write it). */ +export function steppedLoopCounter( + stmt: IrStmt & { kind: "for" }, + locals: ReadonlyMap, +): { ref: IrExpr & { kind: "varRef" }; step: number } | null { + const cond = stmt.cond; + if (cond?.kind !== "bin") return null; + for (const side of [cond.left, cond.right]) { + if (side.kind !== "varRef") continue; + const local = locals.get(side.localId); + if (local === undefined || local.boxed) continue; + const step = constantStep(stmt.update, side.localId); + if (step === null || writesLocal(stmt.body, side.localId)) continue; + if (writesLocal([{ kind: "exprStmt", expr: cond, loc: cond.loc }], side.localId)) continue; + return { ref: side, step }; + } + return null; +} + function isUnitIncrement(update: IrStmt | null, localId: string): boolean { // The frontend normally lowers `i++` to `i = i + 1` before backend // emission. Accept the incDec form too so this analysis remains valid for diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 10e0a9e38..494c4e3e8 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -103,6 +103,11 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "punycode.warn", // DEP0005 for the deprecated Buffer constructor. "buffer.deprecation", + // util.aborted answers a checked-dynamic promise, and so do the + // functions util.debounce and util.throttle return. + "util.aborted", + "util.debounce", + "util.throttle", "timers.immediatePromise", // The performance timeline (scr_perf.c, linked with this unit for its // MaxPerformanceEntryBufferExceededWarning). @@ -144,8 +149,11 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "util.promisifyDyn", "util.debuglog", "util.deprecate", + "util.diff", + "util.extend", "util.inherits", "util.inheritsPrototype", + "util.isArrayDeprecated", "util.styleText", ]); @@ -174,8 +182,11 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([ "util.callbackify", "util.debuglog", "util.deprecate", + "util.diff", + "util.extend", "util.inherits", "util.inheritsPrototype", + "util.isArrayDeprecated", ]); /** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */ @@ -997,9 +1008,20 @@ const RUNTIME_FEATURE_RULES = { (mod.classes ?? []).some( (c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS, ), - fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS], + // util.debounce and util.throttle register options.signal's listener. + fns: [ + "events.addAbortListener", + "util.aborted", + "util.debounce", + "util.throttle", + ...ABORT_SIGNAL_LIB_FNS, + ], prefixes: ["event."], }), + /** util.MIMEType and util.MIMEParams (scr_mime.c). */ + mime: rule({ prefixes: ["mime."] }), + /** util.debounce and util.throttle (scr_util_timing.c). */ + utilTiming: rule({ fns: ["util.debounce", "util.throttle"] }), /** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */ eventsOnceSignal: rule({ fns: ["events.onceSignal"] }), fetch: rule({ prefixes: ["fetch."] }), @@ -1218,6 +1240,8 @@ function noRuntimeFeatures(): RuntimeFeatures { http: false, inspect: false, legacyTextDecoder: false, + mime: false, + utilTiming: false, mockTimers: false, net: false, netLookup: false, diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 771e2a426..c28f7fce4 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -83,6 +83,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( declAdopt: false, dynForAwait: false, eventsOnceSignal: false, + mime: false, + utilTiming: false, dynBuffer: false, builtinValues: false, warningReplace: false, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 5f5d8f7e0..2237619ef 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -2482,12 +2482,36 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.util._extend", + "kind": "node-builtin", + "name": "util._extend", + "status": "static" + }, + { + "id": "node-builtin.util.aborted", + "kind": "node-builtin", + "name": "util.aborted", + "status": "static" + }, { "id": "node-builtin.util.callbackify", "kind": "node-builtin", "name": "util.callbackify", "status": "static" }, + { + "id": "node-builtin.util.convertProcessSignalToExitCode", + "kind": "node-builtin", + "name": "util.convertProcessSignalToExitCode", + "status": "static" + }, + { + "id": "node-builtin.util.debounce", + "kind": "node-builtin", + "name": "util.debounce", + "status": "static" + }, { "id": "node-builtin.util.debug", "kind": "node-builtin", @@ -2506,6 +2530,12 @@ "name": "util.deprecate", "status": "static" }, + { + "id": "node-builtin.util.diff", + "kind": "node-builtin", + "name": "util.diff", + "status": "static" + }, { "id": "node-builtin.util.getSystemErrorMap", "kind": "node-builtin", @@ -2530,6 +2560,12 @@ "name": "util.inherits", "status": "static" }, + { + "id": "node-builtin.util.isArray", + "kind": "node-builtin", + "name": "util.isArray", + "status": "static" + }, { "id": "node-builtin.util.isDeepStrictEqual", "kind": "node-builtin", @@ -2568,6 +2604,12 @@ "name": "util.styleText", "status": "static" }, + { + "id": "node-builtin.util.throttle", + "kind": "node-builtin", + "name": "util.throttle", + "status": "static" + }, { "id": "node-builtin.util.toUSVString", "kind": "node-builtin", diff --git a/packages/compiler/test/counted-loop-emission.test.ts b/packages/compiler/test/counted-loop-emission.test.ts index f617c6ab4..d7ebc97fa 100644 --- a/packages/compiler/test/counted-loop-emission.test.ts +++ b/packages/compiler/test/counted-loop-emission.test.ts @@ -136,3 +136,111 @@ test("worker programs poll counted loops over typed arrays between blocks of ite await rm(dir, { recursive: true, force: true }); } }); + +test("worker programs poll no loop whose proven ranges bound its runs inside another loop", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-bounded-loop-polls-")); + try { + const entry = join(dir, "bounded.ts"); + await writeFile( + entry, + [ + 'import { isMainThread, Worker } from "node:worker_threads";', + "const LIMIT = 50;", + "export function rows(cr: Float64Array, n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let i = 0; i < 8; i += 2) {", + " const c = cr[i]!;", + " for (let j = 0; j < LIMIT; j++) {", + " t = (t * 31 + j + c) % 1000003;", + " if (t === 7) break;", + " }", + " }", + " return t;", + "}", + "export function open(cr: Float64Array, n: number, k: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let i = 0; i < 8; i += 2) {", + " const c = cr[i]!;", + " for (let j = 0; j < k; j++) {", + " t = (t * 31 + j + c) % 1000003;", + " if (t === 7) break;", + " }", + " }", + " return t;", + "}", + "export function wide(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++) for (let j = 0; j < 5000; j++) t = (t * 31 + j) % 1000003;", + " return t;", + "}", + "export function exact(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let j = 9007199254740900; j < 9007199254740950; j++) t = (t + (j % 7)) % 1000003;", + " return t;", + "}", + "export function stalled(n: number): number {", + " let t = 0;", + " for (let r = 0; r < n; r++)", + " for (let j = 9007199254740990; j < 9007199254741002; j++) t = (t + (j % 7)) % 1000003;", + " return t;", + "}", + "export function single(): number {", + " let t = 0;", + " for (let j = 0; j < 50; j++) t = (t * 31 + j) % 1000003;", + " return t;", + "}", + "const cr = new Float64Array(8).fill(0.5);", + 'const k = Number(process.argv[2] ?? "50");', + "console.log(rows(cr, 3), open(cr, 3, k), wide(2), exact(2), stalled(0), single());", + "if (!isMainThread) new Worker(new URL(import.meta.url));", + ].join("\n"), + ); + const outPath = join(dir, "bounded.ir.json"); + const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8"))); + const body = (name: string): string => + [ + ...ll.matchAll( + new RegExp( + `define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`, + "g", + ), + ), + ] + .map((match) => match[1]!) + .join("\n"); + const polls = (text: string): number => text.match(/@scr_exc_flag/g)?.length ?? 0; + // The i and j loops of `rows` run at most 4 × (1 + 50) bodies: only the + // r loop polls, and the i loop over the typed array is not strip-mined. + const rows = body("rows"); + expect(rows).not.toMatch(/^loop\.poll\d+:$/m); + expect(polls(rows)).toBeGreaterThan(0); + // With a limit known only at run time the inner loops keep their polls. + // The innermost one is strip-mined, and a block that ends at the limit + // leaves it without polling: only a block that ends short of it polls. + const open = body("open"); + expect(polls(open)).toBeGreaterThan(polls(rows)); + expect(open).toMatch(/^loop\.poll\d+:$/m); + const ends = [...open.matchAll(/^loop\.end\d+:$[\s\S]*?^\S/gm)].map((match) => match[0]); + expect(ends.length).toBeGreaterThan(0); + for (const end of ends) expect(end).toMatch(/br i1 %\w+, label %loop\.poll\d+, label %/); + // A run of up to 16384 iterations is one block, polled as it leaves when + // it took more than 64. + expect(open).toMatch(/^loop\.poll\.run\d+:$/m); + // Steps of 1 below 2^53 are exact, so 50 runs near it are bounded. From + // 2^53, `j + 1 === j`: a counter ranging up to it can run without end + // and keeps its poll. + expect(polls(body("exact"))).toBe(polls(body("single"))); + expect(polls(body("stalled"))).toBeGreaterThan(polls(body("exact"))); + // A nest over the bound, or a loop no other loop encloses, polls. + expect(polls(body("wide"))).toBeGreaterThan(0); + expect(body("wide")).toMatch(/^loop\.poll\d+:$/m); + expect(polls(body("single"))).toBeGreaterThan(0); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json new file mode 100644 index 000000000..825fd9882 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7520-util-format-runtime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7520-util-format-runtime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json new file mode 100644 index 000000000..0714d5c08 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7521-util-format-runtime.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7521-util-format-runtime.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json new file mode 100644 index 000000000..bcd6b54c6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7522-promisify-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7522-promisify-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json new file mode 100644 index 000000000..20e96495a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7523-promisify-untyped.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7523-promisify-untyped.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json new file mode 100644 index 000000000..4f5a3d007 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7524-inspect-custom.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7524-inspect-custom.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json new file mode 100644 index 000000000..dddad2358 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7525-inspect-custom.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7525-inspect-custom.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json new file mode 100644 index 000000000..70b202903 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7526-inspect-options-runtime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7526-inspect-options-runtime.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7536-inspect-custom-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7536-inspect-custom-errors.ts.json new file mode 100644 index 000000000..2c5325056 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7536-inspect-custom-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7536-inspect-custom-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7537-inspect-custom-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7537-inspect-custom-errors.cjs.json new file mode 100644 index 000000000..16cb948f6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7537-inspect-custom-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7537-inspect-custom-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7538-inspect-custom-prototypes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7538-inspect-custom-prototypes.mjs.json new file mode 100644 index 000000000..279015554 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7538-inspect-custom-prototypes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7538-inspect-custom-prototypes.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7700-util-deprecated-helpers.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7700-util-deprecated-helpers.cjs.json new file mode 100644 index 000000000..3ea85d24f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7700-util-deprecated-helpers.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7700-util-deprecated-helpers.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7701-util-deprecated-helpers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7701-util-deprecated-helpers.ts.json new file mode 100644 index 000000000..10519ed2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7701-util-deprecated-helpers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7701-util-deprecated-helpers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7702-util-signal-exit-code.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7702-util-signal-exit-code.cjs.json new file mode 100644 index 000000000..fe1de1b5f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7702-util-signal-exit-code.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7702-util-signal-exit-code.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7703-util-aborted.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7703-util-aborted.cjs.json new file mode 100644 index 000000000..81b57b493 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7703-util-aborted.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7703-util-aborted.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7704-util-aborted.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7704-util-aborted.ts.json new file mode 100644 index 000000000..d59d1194f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7704-util-aborted.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7704-util-aborted.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7705-util-diff.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7705-util-diff.cjs.json new file mode 100644 index 000000000..630cca625 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7705-util-diff.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7705-util-diff.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7706-util-diff.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7706-util-diff.ts.json new file mode 100644 index 000000000..af97c99f8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7706-util-diff.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7706-util-diff.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7707-util-mime-type.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7707-util-mime-type.cjs.json new file mode 100644 index 000000000..4c1430134 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7707-util-mime-type.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7707-util-mime-type.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7708-util-mime-type.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7708-util-mime-type.ts.json new file mode 100644 index 000000000..2c099bc66 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7708-util-mime-type.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7708-util-mime-type.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7709-function-own-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7709-function-own-properties.cjs.json new file mode 100644 index 000000000..121ca711c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7709-function-own-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7709-function-own-properties.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7710-util-debounce.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7710-util-debounce.cjs.json new file mode 100644 index 000000000..ed7e96287 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7710-util-debounce.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7710-util-debounce.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7711-util-throttle.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7711-util-throttle.cjs.json new file mode 100644 index 000000000..0e5bba052 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7711-util-throttle.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7711-util-throttle.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7712-util-debounce-throttle.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7712-util-debounce-throttle.ts.json new file mode 100644 index 000000000..b9ca2c2c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7712-util-debounce-throttle.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7712-util-debounce-throttle.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json new file mode 100644 index 000000000..872a85396 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8120-inspect-custom-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8120-inspect-custom-nested.ts.json new file mode 100644 index 000000000..49e0de238 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8120-inspect-custom-nested.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8120-inspect-custom-nested.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8121-inspect-custom-nested.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8121-inspect-custom-nested.cjs.json new file mode 100644 index 000000000..27372abb0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8121-inspect-custom-nested.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8121-inspect-custom-nested.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8122-inspect-custom-runtime-only.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8122-inspect-custom-runtime-only.ts.json new file mode 100644 index 000000000..0f25586d4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8122-inspect-custom-runtime-only.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8122-inspect-custom-runtime-only.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json new file mode 100644 index 000000000..1dc598fd4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8125-inspect-custom-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8125-inspect-custom-throws.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json new file mode 100644 index 000000000..374de8db3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8126-inspect-custom-throws.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8126-inspect-custom-throws.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json new file mode 100644 index 000000000..80675e5e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dyn-union-function-arms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dyn-union-function-arms.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json new file mode 100644 index 000000000..bc87c6ea0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-member-lambda-impls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-member-lambda-impls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json new file mode 100644 index 000000000..2e52a519c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-terminate-bounded-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/native-worker-terminate-bounded-loops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json new file mode 100644 index 000000000..abf783f85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/recursive-optional-callback-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/recursive-optional-callback-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json new file mode 100644 index 000000000..dff7b7d6b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/inspect-custom-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/inspect-custom-fences.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index b4868d5cb..49e5896b6 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -93,6 +93,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_events_emitter.c", "emitter"], // events.once with options registers through the emitter and event-target units. ["scr_events_once_signal.c", "eventsOnceSignal"], + ["scr_mime.c", "mime"], + ["scr_util_timing.c", "utilTiming"], ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], ["scr_bigint_assert.c", all("assert", "bigint")], diff --git a/packages/runtime/src/scr_console_native.c b/packages/runtime/src/scr_console_native.c index 8c7467127..492d57d82 100644 --- a/packages/runtime/src/scr_console_native.c +++ b/packages/runtime/src/scr_console_native.c @@ -523,7 +523,11 @@ ScrStr *scr_console_dir_dyn(ScrDyn *value, double depth) { scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); return scr_str_new("", 0); } - return scr_insp_dyn(value, 0, depth); + /* console.dir is inspect with customInspect: false. */ + bool custom = scr_insp_custom_inspect(false); + ScrStr *out = scr_insp_dyn(value, 0, depth); + scr_insp_custom_inspect(custom); + return out; } /* ── console.table ─────────────────────────────────────────────────── diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 1004a93c2..2d916a825 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -186,6 +186,8 @@ typedef enum { X(MATCH_SOURCE, 55) /* a match result's pending metadata (scr_regex.c) */ \ X(MESSAGE_EVENT, 50) /* ScrEvent with data — BroadcastChannel's 'message' */ \ X(MESSAGE_PORT, 36) \ + X(MIME_PARAMS, 81) /* util.MIMEParams (scr_mime.c) */ \ + X(MIME_TYPE, 82) /* util.MIMEType (scr_mime.c) */ \ X(MODULE_NAMESPACE, 79) /* module namespace object read through its exports (scr_module_namespace.c) */ \ X(NET_BLOCK_LIST, 48) /* net.BlockList (scr_net.c) */ \ X(NET_SERVER, 3) /* ScrNetServer — net.Server (http.Server rides the same handle) */ \ @@ -961,6 +963,10 @@ ScrDyn *scr_dyn_open_iterator(const ScrDyn *value, int *kind, bool generators); * for-of protocol reads it. +1, or NULL with the error pending. */ ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len); void scr_dyn_install_iterator_symbol(ScrDyn *key); +/* Symbol.for('nodejs.util.inspect.custom'): the symbol unit installs it + * when the registry creates it; NULL (borrowed) until then. */ +void scr_dyn_install_inspect_custom_symbol(ScrDyn *key); +ScrDyn *scr_dyn_inspect_custom_symbol(void); void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c index 9ca91879a..0f59d8ca7 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -1962,3 +1962,60 @@ ScrPromise *scr_tp_scheduler_wait_signal(double ms, ScrPromise *value, bool ref, ScrPromise *scr_tp_set_immediate_signal(ScrPromise *value, bool ref, ScrDyn *signal) { return scr_tp_signal(0, value, ref, signal, true); } + +/* ── util.aborted(signal, resource) ──────────────────────────────────── + * Node's async function: the signal must be an AbortSignal and the + * resource an object (functions and arrays included), or the promise + * rejects with ERR_INVALID_ARG_TYPE. An aborted signal gives a promise of + * undefined; otherwise a once 'abort' listener that resists + * stopImmediatePropagation resolves it with the event. The answer adopts + * that promise, as an async function's result does. */ +static ScrDyn *scr_util_aborted_listener(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrPromise *inner = scr_box_get_ref(closure->caps[0]); + /* scr_promise_resolve_dyn takes the value's reference. */ + scr_promise_resolve_dyn(inner, scr_dyn_retain(argc ? args[0] : scr_dyn_undefined())); + scr_promise_release(inner); + return scr_dyn_retain(scr_dyn_undefined()); +} + +ScrDyn *scr_util_aborted(ScrDyn *signal, ScrDyn *resource) { + ScrPromise *outer = scr_promise_new(); + bool aborted = false; + ScrEventTargetCore *core = scr_et_signal_core_fn ? scr_et_signal_core_fn(signal, &aborted) : NULL; + bool object = resource->kind == SCR_DYN_OBJ || resource->kind == SCR_DYN_ARR || resource->kind == SCR_DYN_FUNC || + resource->kind == SCR_DYN_TYPED_REF || resource->kind == SCR_DYN_HANDLE || + resource->kind == SCR_DYN_BYTES || resource->kind == SCR_DYN_PROMISE || + resource->kind == SCR_DYN_PROXY; + if (!core) { + static const char message[] = "signal is not of type AbortSignal."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_ARG_TYPE"); + } else if (!object) { + scr_dyn_arg_type_fail("resource", "of type object", resource); + } + if (scr_exc_pending()) { + scr_promise_reject_pending(outer); + ScrDyn *rejected = scr_dyn_new_promise(outer); + scr_promise_release(outer); + return rejected; + } + ScrPromise *inner = scr_promise_new(); + if (aborted) { + scr_promise_fulfill_void(inner); + } else { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, NULL); + scr_box_set_ref(closure->caps[0], scr_promise_retain(inner)); + ScrDyn *listener = scr_dyn_new_func(closure, scr_util_aborted_listener, 1, "native:function", ""); + ScrStr *abort = scr_str_new("abort", 5); + bool duplicate; + (void)scr_et_append(core, abort, listener, SCR_ET_ONCE | SCR_ET_RESIST, &duplicate); + scr_str_release(abort); + scr_dyn_release(listener); + } + ScrDyn *adopted = scr_dyn_new_promise(inner); + scr_promise_release(inner); + scr_promise_resolve_dyn(outer, adopted); /* moves the promise box in */ + ScrDyn *result = scr_dyn_new_promise(outer); + scr_promise_release(outer); + return result; +} diff --git a/packages/runtime/src/scr_event_target.h b/packages/runtime/src/scr_event_target.h index e1ff5d0da..67f8d1544 100644 --- a/packages/runtime/src/scr_event_target.h +++ b/packages/runtime/src/scr_event_target.h @@ -106,6 +106,8 @@ typedef struct { extern SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops; void scr_et_broadcast_install(void); +/* util.aborted(signal, resource): +1 dyn promise; argument errors reject it. */ +ScrDyn *scr_util_aborted(ScrDyn *signal, ScrDyn *resource); /* events.addAbortListener(signal, listener): +1 disposable, or NULL. */ ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener); /* timers/promises setTimeout(delay, value, options) and setImmediate(value, diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index c84fcf4ad..bec5be95a 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -152,6 +152,154 @@ static size_t insp_width(const char *data, size_t len) { return width; } +/* ── colors (stylizeWithColor) ───────────────────────────────────────── */ + +/* ctx.colors for the render in progress: the checked-dynamic walk wraps + * each styled leaf in inspect.colors' SGR codes for its inspect.styles + * style, and the layout measures entries without the codes. Static + * renderings never run with it set. */ +static SCR_TL bool g_colors; + +typedef enum { + INSP_SPECIAL, + INSP_NUMBER, + INSP_BIGINT, + INSP_BOOLEAN, + INSP_UNDEFINED, + INSP_NULL, + INSP_STRING, + INSP_SYMBOL, + INSP_DATE, +} InspStyle; + +/* inspect.styles mapped through inspect.colors: [open, close]. */ +static const unsigned char insp_style_codes[][2] = { + {36, 39}, /* special: cyan */ + {33, 39}, /* number: yellow */ + {33, 39}, /* bigint: yellow */ + {33, 39}, /* boolean: yellow */ + {90, 39}, /* undefined: grey */ + {1, 22}, /* null: bold */ + {32, 39}, /* string: green */ + {32, 39}, /* symbol: green */ + {35, 39}, /* date: magenta */ +}; + +static void insp_style_open(InspBuf *b, InspStyle style) { + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", insp_style_codes[style][0]); + ib_bytes(b, code, (size_t)n); +} + +static void insp_style_close(InspBuf *b, InspStyle style) { + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", insp_style_codes[style][1]); + ib_bytes(b, code, (size_t)n); +} + +/* ctx.stylize(text, style): +1; moves `text` in. */ +static ScrStr *insp_styled(ScrStr *text, InspStyle style) { + if (!g_colors) return text; + InspBuf b = {0}; + insp_style_open(&b, style); + ib_str(&b, text); + insp_style_close(&b, style); + scr_str_release(text); + return ib_take(&b); +} + +static ScrStr *insp_styled_lit(const char *text, size_t len, InspStyle style) { + return insp_styled(scr_str_new(text, len), style); +} + +/* The SC2020 refusal for colored renderings of values whose styles this + * runtime does not produce; answers an empty string. */ +static ScrStr *insp_colors_fence(const char *what) { + InspBuf b = {0}; + ib_cstr(&b, "colored inspect output of "); + ib_cstr(&b, what); + ib_cstr(&b, " has no lowering"); + ScrStr *message = ib_take(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); + return scr_str_new("", 0); +} + +static ScrStr *insp_colors_fence_values(const char *cls) { + InspBuf b = {0}; + ib_cstr(&b, cls); + ib_cstr(&b, " values"); + ScrStr *what = ib_take(&b); + ScrStr *out = insp_colors_fence(what->data); + scr_str_release(what); + return out; +} + +/* The SGR sequence Node's removeColors strips (\x1b[<1 or 2 digits>m) at + * data[i]: its length, or 0. */ +static size_t insp_sgr_at(const char *data, size_t len, size_t i) { + if (data[i] != '\x1b' || i + 3 >= len || data[i + 1] != '[') return 0; + size_t j = i + 2, digits = 0; + while (j < len && digits < 2 && data[j] >= '0' && data[j] <= '9') j++, digits++; + return digits > 0 && j < len && data[j] == 'm' ? j + 1 - i : 0; +} + +/* A control sequence stripVTControlCharacters removes at data[i] (CSI + * sequences after ESC [ or C1 CSI, and OSC sequences ended by BEL): its + * length, or 0. */ +static size_t insp_vt_at(const char *data, size_t len, size_t i) { + const unsigned char *p = (const unsigned char *)data; + size_t j; + if (p[i] == 0x1b && i + 1 < len && p[i + 1] == '[') j = i + 2; + else if (p[i] == 0xc2 && i + 1 < len && p[i + 1] == 0x9b) j = i + 2; + else if (p[i] == 0x1b && i + 1 < len && p[i + 1] == ']') { + for (j = i + 2; j < len; j++) + if (p[j] == 0x07) return j + 1 - i; + return 0; + } else return 0; + while (j < len && ((p[j] >= '0' && p[j] <= '9') || p[j] == ';' || p[j] == '?')) j++; + if (j >= len) return 0; + unsigned char c = p[j]; + bool final = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'P') || (c >= 'R' && c <= 'T') || + c == 'Z' || c == 'c' || (c >= 'f' && c <= 'n') || c == 'q' || c == 'r' || + c == 't' || c == 'y' || c == '=' || c == '>' || c == '<' || c == '~'; + return final ? j + 1 - i : 0; +} + +/* An entry's display width for the grid (getStringWidth(entry, + * ctx.colors)) and its UTF-16 length for the break-length test (Node's + * removeColors under ctx.colors). */ +static size_t insp_entry_width(const ScrStr *s) { + if (!g_colors) return insp_width(s->data, s->len); + size_t width = 0; + for (size_t i = 0; i < s->len;) { + size_t skip = insp_vt_at(s->data, s->len, i); + if (skip) { + i += skip; + continue; + } + uint32_t cp = insp_cp_at(s->data, s->len, &i); + if (insp_full_width(cp)) width += 2; + else if (!insp_zero_width(cp)) width += 1; + } + return width; +} + +static size_t insp_entry_len(const ScrStr *s) { + if (!g_colors) return insp_utf16_len(s->data, s->len); + size_t units = 0; + for (size_t i = 0; i < s->len;) { + size_t skip = insp_sgr_at(s->data, s->len, i); + if (skip) { + i += skip; + continue; + } + uint32_t cp = insp_cp_at(s->data, s->len, &i); + units += cp >= 0x10000 ? 2 : 1; + } + return units; +} + /* ── number rendering ────────────────────────────────────────────────── */ /* formatNumber: JS ToString EXCEPT -0 renders as '-0'. */ @@ -245,12 +393,12 @@ static SCR_TL size_t g_frames_cap; static SCR_TL size_t g_indent; /* ctx.indentationLvl */ static SCR_TL bool g_no_break; /* breakLength: Infinity (console.table cells) */ static SCR_TL double g_break_length = 80; /* ctx.breakLength */ -static SCR_TL double g_compact = 3; /* ctx.compact: a number, 0 for false */ +static SCR_TL double g_compact = 3; /* ctx.compact: a number, -1 for false */ static SCR_TL bool g_sorted; /* ctx.sorted === true */ static SCR_TL double g_cur_depth; /* ctx.currentDepth (last-entered composite) */ /* util.inspect's breakLength, compact and sorted for the renders that - * follow (compact false is 0); the call site restores 80, 3 and false + * follow (compact false is -1); the call site restores 80, 3 and false * after its render. */ void scr_insp_layout(double break_length, double compact, bool sorted) { g_break_length = break_length; @@ -372,7 +520,7 @@ void scr_insp_seen_push(const void *v) { ScrStr *scr_insp_circular(double id) { char buf[32]; int n = snprintf(buf, sizeof buf, "[Circular *%.0f]", id); - return scr_str_new(buf, (size_t)n); + return insp_styled_lit(buf, (size_t)n, INSP_SPECIAL); } /* Pops the value's seen entry after its frame closed. scr_insp_end already @@ -406,7 +554,7 @@ static void insp_group(InspFrame *f, bool trailing_more) { size_t *data_len = malloc((output_len ? output_len : 1) * sizeof(size_t)); if (!data_len) insp_oom(); for (size_t i = 0; i < output_len; i++) { - size_t w = insp_width(f->items[i]->data, f->items[i]->len); + size_t w = insp_entry_width(f->items[i]); data_len[i] = w; total_length += (double)w + 2; if ((double)w > max_length) max_length = (double)w; @@ -497,7 +645,7 @@ static bool insp_below_break_length(InspFrame *f, size_t start, const ScrStr *ba size_t total = f->len + start; if ((double)(total + f->len) > g_break_length) return false; for (size_t i = 0; i < f->len; i++) { - total += insp_utf16_len(f->items[i]->data, f->items[i]->len); + total += insp_entry_len(f->items[i]); if ((double)total > g_break_length) return false; } return base->len == 0 || !insp_contains_byte(base->data, base->len, '\n'); @@ -523,12 +671,16 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, bool int ref_id = f->seen ? insp_circ_id(f->seen) : 0; if (ref_id) { char buf[32]; - int n = snprintf(buf, sizeof buf, base->len ? " " : "", ref_id); - size_t len = (size_t)n + base->len; - ref_base = scr_str_alloc_raw(len, len); - memcpy(ref_base->data, buf, (size_t)n); - memcpy(ref_base->data + n, base->data, base->len); - ref_base->data[len] = '\0'; + int n = snprintf(buf, sizeof buf, "", ref_id); + InspBuf rb = {0}; + if (g_colors) insp_style_open(&rb, INSP_SPECIAL); + ib_bytes(&rb, buf, (size_t)n); + if (g_colors) insp_style_close(&rb, INSP_SPECIAL); + if (base->len) { + ib_char(&rb, ' '); + ib_str(&rb, base); + } + ref_base = ib_take(&rb); base = ref_base; } size_t entries = f->len; @@ -625,6 +777,19 @@ void scr_insp_nested_begin(void) { g_ncirc = 0; } +/* nested_end's restore alone: the caller's context returns. */ +void scr_insp_nested_restore(void) { + InspSaved *sv = &g_saved[--g_nsaved]; + free(g_seen); + g_indent = sv->indent; + g_cur_depth = sv->cur_depth; + g_seen = sv->seen; + g_nseen = sv->nseen; + g_seen_cap = sv->seen_cap; + memcpy(g_circ, sv->circ, sizeof g_circ); + g_ncirc = sv->ncirc; +} + ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner) { InspSaved *sv = &g_saved[--g_nsaved]; free(g_seen); @@ -656,10 +821,14 @@ typedef struct { size_t nframes; size_t nsaved; size_t nseen; + size_t ncall; size_t indent; double cur_depth; } InspMark; +static size_t insp_call_depth(void); /* custom calls open, below */ +static void insp_call_unwind(size_t ncall); /* their options, below */ + static SCR_TL InspMark *g_marks; static SCR_TL size_t g_nmarks; static SCR_TL size_t g_marks_cap; @@ -670,11 +839,12 @@ void scr_insp_mark(void) { g_marks = realloc(g_marks, g_marks_cap * sizeof(InspMark)); if (!g_marks) insp_oom(); } - g_marks[g_nmarks++] = (InspMark){g_nframes, g_nsaved, g_nseen, g_indent, g_cur_depth}; + g_marks[g_nmarks++] = (InspMark){g_nframes, g_nsaved, g_nseen, insp_call_depth(), g_indent, g_cur_depth}; } void scr_insp_unwind(void) { InspMark m = g_marks[--g_nmarks]; + insp_call_unwind(m.ncall); while (g_nframes > m.nframes) { InspFrame *f = &g_frames[--g_nframes]; for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); @@ -768,13 +938,17 @@ ScrStr *scr_insp_str(ScrStr *s) { ib_cstr(&out, " +\n"); ib_spaces(&out, g_indent + 2); } + if (g_colors) insp_style_open(&out, INSP_STRING); insp_quote_into(&out, v + chunk_start, end - chunk_start); + if (g_colors) insp_style_close(&out, INSP_STRING); chunk_start = end; first = false; } } } else { + if (g_colors) insp_style_open(&out, INSP_STRING); insp_quote_into(&out, v, vlen); + if (g_colors) insp_style_close(&out, INSP_STRING); } if (trailer[0]) ib_cstr(&out, trailer); free(val.data); @@ -1130,7 +1304,9 @@ static void insp_key_into(InspBuf *b, const char *key, size_t len) { if (bare) { ib_bytes(b, key, len); } else { + if (g_colors) insp_style_open(b, INSP_STRING); insp_quote_into(b, key, len); + if (g_colors) insp_style_close(b, INSP_STRING); } } @@ -1467,7 +1643,7 @@ static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, ib_char(&out, '['); ib_cstr(&out, label); ib_char(&out, ']'); - return ib_take(&out); + return insp_styled(ib_take(&out), INSP_SPECIAL); } ScrStr *base = ib_take(&head); scr_insp_begin(recurse + 1); @@ -1539,9 +1715,9 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char : scr_str_new("{}", 2); } if (recurse > depth) { - if (cls) return scr_insp_class_tag(cls, NULL); - return d->null_proto ? scr_str_new("[Object: null prototype]", 24) - : scr_str_new("[Object]", 8); + if (cls) return insp_styled(scr_insp_class_tag(cls, NULL), INSP_SPECIAL); + return d->null_proto ? insp_styled_lit("[Object: null prototype]", 24, INSP_SPECIAL) + : insp_styled_lit("[Object]", 8, INSP_SPECIAL); } scr_insp_begin(recurse + 1); for (size_t i = 0; i < d->v.obj.len; i++) { @@ -1563,6 +1739,7 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char ScrStr *entry = ib_take(&eb); scr_insp_entry(entry, false); scr_str_release(entry); + if (scr_exc_pending()) break; } for (size_t i = 0; i < nsymbols; i++) { ScrDynEntry *ent = scr_insp_symbol_entry(d, i); @@ -1612,12 +1789,12 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { if (recurse > depth) { char tag[32]; int n = snprintf(tag, sizeof tag, "[%s]", scr_bytes_elem_name(b->elem)); - return scr_str_new(tag, (size_t)n); + return insp_styled_lit(tag, (size_t)n, INSP_SPECIAL); } scr_insp_begin(recurse + 1); size_t shown = b->len < 100 ? b->len : 100; for (size_t i = 0; i < shown; i++) { - ScrStr *s = scr_insp_f64(scr_bytes_get(b, (double)i)); + ScrStr *s = insp_styled(scr_insp_f64(scr_bytes_get(b, (double)i)), INSP_NUMBER); scr_insp_entry(s, true); scr_str_release(s); } @@ -1646,6 +1823,473 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { return out; } +/* ── custom inspect functions (util.inspect.custom) ────────────────────── + * formatValue's hook: an object whose [util.inspect.custom] member is a + * function (other than util.inspect itself, on an object that is not its + * constructor's prototype) renders as that function's result. It runs + * before the circular and depth checks, with the remaining depth + * (ctx.depth - recurseTimes), the options object getUserOptions builds and + * util.inspect. A string result is re-indented at the current level; the + * object itself renders normally; any other value renders in its place. + * Class instances held as checked-dynamic values have compiled methods + * only, so one whose class defines the method throws SC2020. */ +static SCR_TL bool g_custom_inspect = true; /* ctx.customInspect */ +/* ctx.depth as the options gave it, for the custom functions' depth + * argument and options when the render's depth is unbounded: 0 the number + * g_depth_number (Infinity or NaN), 1 null, 2 undefined. */ +static SCR_TL int g_depth_kind = 1; +static SCR_TL double g_depth_number; +/* The options object that held keys inspect does not read: getUserOptions + * spreads it into the options the custom functions receive (borrowed for + * the render). */ +static SCR_TL const ScrDyn *g_user_options; + +double scr_insp_depth_kind(double kind) { + double previous = g_depth_kind; + g_depth_kind = (int)kind; + g_depth_number = INFINITY; + return previous; +} + +bool scr_insp_custom_inspect(bool enabled) { + bool previous = g_custom_inspect; + g_custom_inspect = enabled; + return previous; +} + +/* The walk's options while a custom inspect function runs. */ +typedef struct { + bool colors; + bool show_hidden; + bool custom_inspect; + bool sorted; + bool no_break; + int depth_kind; + double depth_number; + double break_length; + double compact; + const ScrDyn *user_options; + size_t nsaved; /* the nested-context stack's depth before the call */ + size_t nframes; /* the frame stack's depth before the call */ +} InspCallSaved; + +static SCR_TL InspCallSaved *g_call_saved; +static SCR_TL size_t g_ncall_saved; +static SCR_TL size_t g_call_saved_cap; + +static void insp_call_saved_cleanup(void) { + free(g_call_saved); + g_call_saved = NULL; + g_ncall_saved = g_call_saved_cap = 0; +} + +/* A custom inspect function's call: Node gives each util.inspect and + * util.format call its own ctx, so the calls the function makes start + * with no indentation, no seen objects, no colors and the default layout + * and options. begin answers a token; end(token) restores the walk's + * context as it was at that begin. A call that threw skips its end, and + * an enclosing custom function's end (or the walk's, when the function + * caught the error) discards what the throw left: the contexts of calls + * that never ended and the frames of renders that never finished. */ +double scr_insp_custom_call_begin(void) { + size_t nsaved = g_nsaved; + scr_insp_nested_begin(); + if (g_ncall_saved == g_call_saved_cap) { + if (g_call_saved_cap == 0) scr_atexit(insp_call_saved_cleanup); + g_call_saved_cap = g_call_saved_cap ? g_call_saved_cap * 2 : 4; + g_call_saved = realloc(g_call_saved, g_call_saved_cap * sizeof(InspCallSaved)); + if (!g_call_saved) insp_oom(); + } + double token = (double)g_ncall_saved; + InspCallSaved *sv = &g_call_saved[g_ncall_saved++]; + sv->nsaved = nsaved; + sv->nframes = g_nframes; + sv->colors = g_colors; + sv->show_hidden = g_show_hidden; + sv->custom_inspect = g_custom_inspect; + sv->sorted = g_sorted; + sv->no_break = g_no_break; + sv->depth_kind = g_depth_kind; + sv->depth_number = g_depth_number; + sv->break_length = g_break_length; + sv->compact = g_compact; + sv->user_options = g_user_options; + g_colors = false; + g_show_hidden = false; + g_custom_inspect = true; + g_no_break = false; + g_depth_kind = 1; + g_depth_number = 0; + g_user_options = NULL; + scr_insp_layout(80, 3, false); + return token; +} + +/* Restores the options saved by the custom call at `depth` and drops it + * and every call above it; NULL when there is none. */ +static InspCallSaved *insp_call_options_restore(size_t depth) { + if (depth >= g_ncall_saved) return NULL; + InspCallSaved *sv = &g_call_saved[depth]; + g_ncall_saved = depth; + g_colors = sv->colors; + g_show_hidden = sv->show_hidden; + g_custom_inspect = sv->custom_inspect; + g_no_break = sv->no_break; + g_depth_kind = sv->depth_kind; + g_depth_number = sv->depth_number; + g_user_options = sv->user_options; + scr_insp_layout(sv->break_length, sv->compact, sv->sorted); + return sv; +} + +void scr_insp_custom_call_end(double token) { + InspCallSaved *sv = insp_call_options_restore((size_t)token); + if (!sv) return; /* an enclosing end already restored it */ + while (g_nframes > sv->nframes) { + InspFrame *f = &g_frames[--g_nframes]; + for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); + free(f->items); + } + g_pending_seen = NULL; + while (g_nsaved > sv->nsaved) scr_insp_nested_restore(); +} + +/* scr_insp_unwind's share: the options of custom calls the refused render + * left open. */ +static void insp_call_unwind(size_t ncall) { insp_call_options_restore(ncall); } + +static size_t insp_call_depth(void) { return g_ncall_saved; } + +/* The style inspect.styles maps a styleType to, as [open, close] codes: + * the styles above plus regexp (red) and module (underline). false when + * the type has no style. */ +static bool insp_style_of(const ScrStr *type, unsigned *open, unsigned *close) { + static const struct { + const char *name; + unsigned open, close; + } styles[] = { + {"special", 36, 39}, {"number", 33, 39}, {"bigint", 33, 39}, {"boolean", 33, 39}, + {"undefined", 90, 39}, {"null", 1, 22}, {"string", 32, 39}, {"symbol", 32, 39}, + {"date", 35, 39}, {"regexp", 31, 39}, {"module", 4, 24}, + }; + for (size_t i = 0; i < sizeof styles / sizeof *styles; i++) { + if (type->len == strlen(styles[i].name) && memcmp(type->data, styles[i].name, type->len) == 0) { + *open = styles[i].open; + *close = styles[i].close; + return true; + } + } + return false; +} + +/* options.stylize: stylizeWithColor under colors (the text in its style's + * codes, or unchanged), stylizeNoColor otherwise (the argument itself). + * caps[0] holds whether colors apply. */ +static ScrDyn *insp_stylize_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *colors = scr_box_get_ref(closure->caps[0]); + bool colored = colors && scr_dyn_truthy(colors); + scr_dyn_release(colors); + ScrDyn *text = argc > 0 ? args[0] : (ScrDyn *)scr_dyn_undefined(); + if (!colored) return scr_dyn_retain(text); + ScrStr *str = scr_dyn_string_coerce_js(text); + if (!str) return NULL; + unsigned open, close; + ScrStr *type = argc > 1 && args[1]->kind == SCR_DYN_STR ? args[1]->v.str : NULL; + if (!type || !insp_style_of(type, &open, &close)) { + ScrDyn *out = scr_dyn_new_str(str); + scr_str_release(str); + return out; + } + InspBuf b = {0}; + char code[8]; + int n = snprintf(code, sizeof code, "\x1b[%um", open); + ib_bytes(&b, code, (size_t)n); + ib_str(&b, str); + n = snprintf(code, sizeof code, "\x1b[%um", close); + ib_bytes(&b, code, (size_t)n); + scr_str_release(str); + ScrStr *styled = ib_take(&b); + ScrDyn *out = scr_dyn_new_str(styled); + scr_str_release(styled); + return out; +} + +static ScrDyn *insp_inspect_call(ScrClosure *closure, ScrDyn *const *args, size_t argc); + +/* Whether `value` is the stylize function getUserOptions built for these + * colors. */ +static bool insp_own_stylize(const ScrDyn *value, bool colors) { + if (value->kind != SCR_DYN_FUNC || value->v.fn.thunk != insp_stylize_call) return false; + ScrDyn *flag = scr_box_get_ref(value->v.fn.clo->caps[0]); + bool colored = flag && scr_dyn_truthy(flag); + scr_dyn_release(flag); + return colored == colors; +} + +/* util.inspect as a function value (the custom functions' third + * argument): a fresh inspect context over the call's arguments, the + * caller's restored afterwards. One function per thread, so the argument + * compares equal to util.inspect. +1. */ +static SCR_TL ScrDyn *g_inspect_function; + +static void insp_inspect_function_cleanup(void) { + scr_dyn_release(g_inspect_function); + g_inspect_function = NULL; +} + +/* util.inspect's own members (custom, colors, styles, defaultOptions) + * read or written through the function value: the tables are not modeled, + * so each throws SC2020 instead of answering undefined. */ +static ScrDyn *insp_inspect_member_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; (void)argc; + char message[128]; + int n = snprintf(message, sizeof message, "util.inspect.%s through the inspect function is not supported yet", + (const char *)closure->fn); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +static ScrDyn *insp_inspect_value(void) { + if (!g_inspect_function) { + ScrClosure *closure = scr_closure_new(NULL, 0); + g_inspect_function = + scr_dyn_new_func(closure, insp_inspect_call, 2, "native:util.inspect", "inspect"); + scr_atexit(insp_inspect_function_cleanup); + static const char *const members[] = {"custom", "defaultOptions", "colors", "styles"}; + ScrDyn *table = scr_dyn_function_properties(g_inspect_function); + for (size_t i = 0; i < sizeof members / sizeof members[0]; i++) { + ScrDyn *get = scr_dyn_new_func(scr_closure_new((void *)members[i], 0), insp_inspect_member_call, 0, + "native:function", members[i]); + ScrDyn *set = scr_dyn_new_func(scr_closure_new((void *)members[i], 0), insp_inspect_member_call, 1, + "native:function", members[i]); + scr_dyn_obj_set_accessor(table, members[i], strlen(members[i]), get, set); + table->v.obj.entries[table->v.obj.len - 1].enumerable = false; + } + scr_dyn_release(table); + } + return scr_dyn_retain(g_inspect_function); +} + +static void insp_opt_set(ScrDyn *options, const char *key, ScrDyn *value /* moves in */) { + scr_dyn_obj_set(options, key, strlen(key), value); +} + +/* getUserOptions(ctx) for the render in progress at `depth`. +1. */ +static ScrDyn *insp_user_options(double depth) { + ScrDyn *options = scr_dyn_new_obj(); + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[0], scr_dyn_new_bool(g_colors)); + insp_opt_set(options, "stylize", + scr_dyn_new_func(closure, insp_stylize_call, 2, "native:util.stylize", + g_colors ? "stylizeWithColor" : "stylizeNoColor")); + insp_opt_set(options, "showHidden", scr_dyn_new_bool(false)); + insp_opt_set(options, "depth", + isfinite(depth) ? scr_dyn_new_num(depth) + : g_depth_kind == 0 ? scr_dyn_new_num(g_depth_number) + : g_depth_kind == 1 ? scr_dyn_new_null() + : scr_dyn_retain(scr_dyn_undefined())); + insp_opt_set(options, "colors", scr_dyn_new_bool(g_colors)); + insp_opt_set(options, "customInspect", scr_dyn_new_bool(true)); + insp_opt_set(options, "showProxy", scr_dyn_new_bool(false)); + insp_opt_set(options, "maxArrayLength", scr_dyn_new_num(100)); + insp_opt_set(options, "maxStringLength", scr_dyn_new_num(10000)); + insp_opt_set(options, "breakLength", scr_dyn_new_num(g_break_length)); + insp_opt_set(options, "compact", g_compact < 0 ? scr_dyn_new_bool(false) : scr_dyn_new_num(g_compact)); + insp_opt_set(options, "sorted", scr_dyn_new_bool(g_sorted)); + insp_opt_set(options, "getters", scr_dyn_new_bool(false)); + insp_opt_set(options, "numericSeparator", scr_dyn_new_bool(false)); + if (g_user_options && g_user_options->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < g_user_options->v.obj.len; i++) { + const ScrDynEntry *ent = &g_user_options->v.obj.entries[i]; + if (!ent->enumerable) continue; + ScrDyn *value = scr_dyn_obj_read((ScrDyn *)g_user_options, ent->key, ent->key_len); + if (!value) { + scr_dyn_release(options); + return NULL; + } + scr_dyn_obj_set(options, ent->key, ent->key_len, value); + } + } + return options; +} + +/* The custom inspect functions of the program's classes, by the type key + * of their instances' typed refs: compiled functions (self, depth, + * options, inspect) that call the instance's method and answer its + * result as a checked-dynamic value. The program registers them before + * it runs. */ +typedef struct { + char *type_key; + ScrDyn *fn; +} InspCustomClass; + +static SCR_TL InspCustomClass *g_custom_classes; +static SCR_TL size_t g_ncustom_classes; + +static void insp_custom_classes_cleanup(void) { + for (size_t i = 0; i < g_ncustom_classes; i++) { + free(g_custom_classes[i].type_key); + scr_dyn_release(g_custom_classes[i].fn); + } + free(g_custom_classes); + g_custom_classes = NULL; + g_ncustom_classes = 0; +} + +void scr_insp_register_custom(ScrStr *type_key, ScrDyn *fn) { + if (g_ncustom_classes == 0) scr_atexit(insp_custom_classes_cleanup); + InspCustomClass *grown = + realloc(g_custom_classes, (g_ncustom_classes + 1) * sizeof *g_custom_classes); + if (!grown) insp_oom(); + g_custom_classes = grown; + char *key = malloc(type_key->len + 1); + if (!key) insp_oom(); + memcpy(key, type_key->data, type_key->len); + key[type_key->len] = '\0'; + g_custom_classes[g_ncustom_classes].type_key = key; + g_custom_classes[g_ncustom_classes].fn = scr_dyn_retain(fn); + g_ncustom_classes++; +} + +static ScrDyn *insp_custom_class_fn(const ScrDyn *d) { + for (size_t i = 0; i < g_ncustom_classes; i++) + if (strcmp(g_custom_classes[i].type_key, d->v.typed_ref.type_key) == 0) + return g_custom_classes[i].fn; + return NULL; +} + +/* The [util.inspect.custom] result for an object kind, or NULL when the + * value renders normally (*thrown reports a pending exception). +1. */ +static ScrStr *insp_custom(ScrDyn *d, double recurse, double depth, bool *thrown) { + *thrown = false; + if (!g_custom_inspect) return NULL; + ScrDyn *fn; + bool compiled = d->kind == SCR_DYN_TYPED_REF; + if (compiled) { + /* A class instance: its class's compiled function, when it has a + * custom inspect method. A class whose method has no compiled call + * (the compiler refuses such classes where it renders them) refuses + * here too. */ + fn = insp_custom_class_fn(d); + if (!fn) { + ScrStr *name = scr_str_new("%symbol:Symbol(nodejs.util.inspect.custom)", 42); + bool declared = scr_dyn_typed_ref_class_has(d, name); + scr_str_release(name); + if (!declared) return NULL; + static const char message[] = + "util.inspect of this class instance's [util.inspect.custom] method has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + *thrown = true; + return NULL; + } + fn = scr_dyn_retain(fn); + } else { + ScrDyn *key = scr_dyn_inspect_custom_symbol(); + if (!key) return NULL; + if (d->kind != SCR_DYN_OBJ && d->kind != SCR_DYN_ARR && d->kind != SCR_DYN_FUNC) return NULL; + fn = scr_dyn_reflect_get(d, key, d); + if (!fn) { + *thrown = true; + return NULL; + } + } + if (fn->kind != SCR_DYN_FUNC || fn->v.fn.thunk == insp_inspect_call) { + scr_dyn_release(fn); + return NULL; + } + /* A constructor's prototype object renders normally. */ + ScrDyn *ctor = !compiled && d->kind == SCR_DYN_OBJ ? scr_dyn_obj_read(d, "constructor", 11) : NULL; + if (ctor && ctor->kind == SCR_DYN_FUNC) { + ScrDyn *prototype = scr_dyn_fn_get(ctor, "prototype", 9); + bool own_prototype = prototype == d; + scr_dyn_release(prototype); + if (own_prototype) { + scr_dyn_release(ctor); + scr_dyn_release(fn); + return NULL; + } + } + scr_dyn_release(ctor); + ScrDyn *args[3]; + /* ctx.depth - recurseTimes, or null for a null depth. */ + args[0] = scr_insp_custom_depth(recurse, depth); + args[1] = insp_user_options(depth); + if (!args[1]) { + scr_dyn_release(args[0]); + scr_dyn_release(fn); + *thrown = true; + return NULL; + } + args[2] = insp_inspect_value(); + ScrDyn *ret; + double token = scr_insp_custom_call_begin(); + if (compiled) { + ScrDyn *call[4] = {d, args[0], args[1], args[2]}; + ret = scr_dyn_call(fn, call, 4, "[nodejs.util.inspect.custom]"); + } else { + scr_dyn_this_push_dyn(d); + ret = scr_dyn_call(fn, args, 3, "[nodejs.util.inspect.custom]"); + scr_dyn_this_pop(); + } + scr_insp_custom_call_end(token); + for (size_t i = 0; i < 3; i++) scr_dyn_release(args[i]); + scr_dyn_release(fn); + if (!ret) { + *thrown = true; + return NULL; + } + if (ret == d || scr_insp_custom_is_self(ret, d)) { + scr_dyn_release(ret); + return NULL; + } + ScrStr *out; + if (ret->kind == SCR_DYN_STR) out = scr_insp_custom_text(ret->v.str); + else out = scr_insp_dyn(ret, recurse, depth); + scr_dyn_release(ret); + if (scr_exc_pending()) { + scr_str_release(out); + *thrown = true; + return NULL; + } + return out; +} + +bool scr_insp_custom_enabled(void) { return g_custom_inspect; } + +/* ctx.depth - recurseTimes, or null for a null depth (NaN for undefined). */ +ScrDyn *scr_insp_custom_depth(double recurse, double depth) { + if (isfinite(depth)) return scr_dyn_new_num(depth - recurse); + if (g_depth_kind == 0) return scr_dyn_new_num(g_depth_number - recurse); + return g_depth_kind == 1 ? scr_dyn_new_null() : scr_dyn_new_num(NAN); +} + +ScrDyn *scr_insp_custom_options(double depth) { return insp_user_options(depth); } + +ScrDyn *scr_insp_inspect_function(void) { return insp_inspect_value(); } + +bool scr_insp_custom_is_self(ScrDyn *result, ScrDyn *self) { + return result->kind == SCR_DYN_TYPED_REF && self->kind == SCR_DYN_TYPED_REF && + result->v.typed_ref.ptr == self->v.typed_ref.ptr; +} + +ScrStr *scr_insp_custom_result(ScrDyn *result, double recurse, double depth) { + if (result->kind == SCR_DYN_STR) return scr_insp_custom_text(result->v.str); + return scr_insp_dyn(result, recurse, depth); +} + +/* A custom function's string result: each newline followed by the + * current indentation. +1. */ +ScrStr *scr_insp_custom_text(ScrStr *text) { + if (!memchr(text->data, '\n', text->len)) return scr_str_retain(text); + InspBuf out = {0}; + for (size_t i = 0; i < text->len; i++) { + ib_char(&out, text->data[i]); + if (text->data[i] == '\n') ib_spaces(&out, g_indent); + } + return ib_take(&out); +} + /* Node's function rendering: [Function: name] / [Function (anonymous)], * and [class C extends B] / [class (anonymous)] for a class. The boxed * name is the compiler's best-effort static spelling (SEMANTICS.md — @@ -1671,12 +2315,22 @@ static ScrStr *scr_insp_dyn_func(ScrDyn *d, double recurse, double depth) { ib_cstr(&out, base->v.fn.name); } ib_char(&out, ']'); - } else if (name && name[0]) { - ib_cstr(&out, "[Function: "); - ib_cstr(&out, name); - ib_char(&out, ']'); } else { - ib_cstr(&out, "[Function (anonymous)]"); + /* The base names the function's own string `name` when its table has + * one (Object.defineProperty, util.callbackify, util.debounce). */ + size_t name_len = name ? strlen(name) : 0; + const ScrDyn *own_name = table && table->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(table, "name", 4) : NULL; + if (own_name && own_name->kind == SCR_DYN_STR) { + name = own_name->v.str->data; + name_len = own_name->v.str->len; + } + if (name_len) { + ib_cstr(&out, "[Function: "); + ib_bytes(&out, name, name_len); + ib_char(&out, ']'); + } else { + ib_cstr(&out, "[Function (anonymous)]"); + } } ScrStr *tag = ib_take(&out); if (cls && !props && cls->own_static_fields) { @@ -1695,7 +2349,7 @@ static ScrStr *scr_insp_dyn_func(ScrDyn *d, double recurse, double depth) { } if (shown == 0) { scr_dyn_release(table); - return tag; + return insp_styled(tag, INSP_SPECIAL); } /* A function already on the traversal stack renders as [Circular *N], * even past the depth limit. Its identity is the class object for a class @@ -1711,8 +2365,9 @@ static ScrStr *scr_insp_dyn_func(ScrDyn *d, double recurse, double depth) { if (recurse > depth) { scr_dyn_release(table); scr_str_release(tag); - return scr_str_new("[Function]", 10); + return insp_styled_lit("[Function]", 10, INSP_SPECIAL); } + tag = insp_styled(tag, INSP_SPECIAL); char *prefix = malloc(tag->len + 1); if (!prefix) scr_trap("scriptc: out of memory\n"); memcpy(prefix, tag->data, tag->len); @@ -1738,8 +2393,10 @@ static const char *insp_hidden_refusal(const ScrDyn *d) { case SCR_DYN_BYTES: return d->buffer ? "Buffer" : d->v.bytes->is_data_view ? "DataView" : "typed array"; case SCR_DYN_OBJ: return scr_dyn_obj_get(d, "%error", 6) != NULL ? "Error" : NULL; case SCR_DYN_HANDLE: + /* A MIMEParams keeps its entries private: Node prints MIMEParams {} + * with or without showHidden. */ return d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_SET || - d->v.handle.tag == SCR_DYNH_DATE + d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_MIME_PARAMS ? NULL : scr_dyn_handle_cls(d); default: return NULL; @@ -1802,6 +2459,14 @@ static ScrStr *scr_insp_search_params(ScrSearchParams *sp, double recurse, doubl ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth); + if ((d->kind == SCR_DYN_TYPED_REF ? g_ncustom_classes > 0 : scr_dyn_inspect_custom_symbol() != NULL) && + (d->kind == SCR_DYN_OBJ || d->kind == SCR_DYN_ARR || d->kind == SCR_DYN_FUNC || + d->kind == SCR_DYN_TYPED_REF)) { + bool thrown; + ScrStr *custom = insp_custom(d, recurse, depth, &thrown); + if (custom) return custom; + if (thrown) return scr_str_new("", 0); + } const char *refused = g_show_hidden ? insp_hidden_refusal(d) : NULL; if (refused) { InspBuf out = {0}; @@ -1813,17 +2478,17 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } switch (d->kind) { case SCR_DYN_NULL: - return scr_str_new("null", 4); + return insp_styled_lit("null", 4, INSP_NULL); case SCR_DYN_UNDEF: - return scr_str_new("undefined", 9); + return insp_styled_lit("undefined", 9, INSP_UNDEFINED); case SCR_DYN_BOOL: - return d->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); + return d->v.b ? insp_styled_lit("true", 4, INSP_BOOLEAN) : insp_styled_lit("false", 5, INSP_BOOLEAN); case SCR_DYN_BIGINT: - return scr_bigint_inspect(d->v.bigint); + return insp_styled(scr_bigint_inspect(d->v.bigint), INSP_BIGINT); case SCR_DYN_SYMBOL: - return d->v.symbol.render(d->v.symbol.value); + return insp_styled(d->v.symbol.render(d->v.symbol.value), INSP_SYMBOL); case SCR_DYN_NUM: - return scr_insp_f64(d->v.num); + return insp_styled(scr_insp_f64(d->v.num), INSP_NUMBER); case SCR_DYN_STR: return scr_insp_str(d->v.str); case SCR_DYN_ARR: { @@ -1835,7 +2500,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { size_t nprops = 0; for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += insp_obj_shows(&props->v.obj.entries[i]); if (len == 0 && nprops == 0 && !g_show_hidden) return scr_str_new("[]", 2); - if (recurse > depth) return scr_str_new("[Array]", 7); + if (recurse > depth) return insp_styled_lit("[Array]", 7, INSP_SPECIAL); scr_insp_begin(recurse + 1); /* Node's maxArrayLength (100) counts output entries; a run of holes * is one `` entry (formatSpecialArray). */ @@ -1848,7 +2513,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { while (next < len && !presence[next]) next++; char hole[48]; int hn = snprintf(hole, sizeof hole, "<%zu empty item%s>", next - i, next - i == 1 ? "" : "s"); - ScrStr *s = scr_str_new(hole, (size_t)hn); + ScrStr *s = insp_styled_lit(hole, (size_t)hn, INSP_UNDEFINED); scr_insp_entry(s, false); scr_str_release(s); i = next; @@ -1858,6 +2523,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { ScrStr *s = scr_insp_dyn(item, recurse + 1, depth); scr_insp_entry(s, item->kind == SCR_DYN_NUM); scr_str_release(s); + if (scr_exc_pending()) break; } bool more = i < len; size_t entries = shown + (more ? 1 : 0); @@ -1916,7 +2582,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { /* An Error carried as a checked-dynamic value (a caught error, an * Error stored in an `unknown` slot): the error's own rendering, * never the encoding's internal marker. */ - if (scr_dyn_obj_get(d, "%error", 6) != NULL) return scr_insp_dyn_error(d, recurse, depth); + if (scr_dyn_obj_get(d, "%error", 6) != NULL) { + if (g_colors) return insp_colors_fence("Error values"); + return scr_insp_dyn_error(d, recurse, depth); + } /* Object.create(null)'s dictionary: Node prefixes the rendering * with "[Object: null prototype]" (formatValue's constructor-less * base), the empty form included, and the beyond-depth answer IS @@ -1926,6 +2595,9 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { case SCR_DYN_FUNC: return scr_insp_dyn_func(d, recurse, depth); case SCR_DYN_HANDLE: { + if (g_colors && d->v.handle.tag != SCR_DYNH_SET && d->v.handle.tag != SCR_DYNH_MAP && + d->v.handle.tag != SCR_DYNH_DATE) + return insp_colors_fence_values(scr_dyn_handle_cls(d)); if (d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER) { return scr_insp_array_buffer(d->v.handle.ptr, scr_dyn_handle_cls(d), recurse, depth); @@ -1935,7 +2607,8 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET) return scr_insp_weak_collection(d->v.handle.tag == SCR_DYNH_WEAK_MAP ? "WeakMap" : "WeakSet", recurse, depth); - if (d->v.handle.tag == SCR_DYNH_DATE) return scr_insp_date(scr_dyn_native_date_value(d)); + if (d->v.handle.tag == SCR_DYNH_DATE) + return insp_styled(scr_insp_date(scr_dyn_native_date_value(d)), INSP_DATE); if (d->v.handle.tag == SCR_DYNH_SEARCH_PARAMS) return scr_insp_search_params(d->v.handle.ptr, recurse, depth); if (d->v.handle.tag == SCR_DYNH_ITERATOR) { @@ -2004,9 +2677,14 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_TYPED_REF: { + if (g_colors && scr_insp_is_generator(d)) return insp_colors_fence("generator objects"); if (scr_insp_is_generator(d)) return scr_insp_generator(d); const char *cls = NULL; ScrDyn *view = scr_event_ref_hooks ? scr_event_ref_hooks->inspect_view(d, &cls) : NULL; + if (view && g_colors) { + scr_dyn_release(view); + return insp_colors_fence_values(cls); + } if (view) { /* The handle rule above: the class name past the depth limit. */ if (recurse > depth) { @@ -2036,6 +2714,11 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { double time; ScrStr *date_class = materialized->kind == SCR_DYN_OBJ && scr_dyn_date_class_time(d, &time) ? scr_dyn_date_class_name(d) : NULL; + if (date_class && g_colors) { + scr_str_release(date_class); + scr_dyn_release(materialized); + return insp_colors_fence("Date subclass instances"); + } if (date_class) { /* An instance of a class declared `extends Date`: the class name and * the time as Date.prototype.toISOString formats it, then its own @@ -2109,6 +2792,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_BYTES: { ScrBytes *b = d->v.bytes; + if (b->is_data_view && g_colors) return insp_colors_fence("DataView values"); if (b->is_data_view) return scr_insp_data_view(b, recurse, depth); if (d->buffer) { /* Buffer-flavored (a stream chunk): Node's form. */ @@ -2128,6 +2812,569 @@ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth) { return scr_insp_dyn(d, 0, depth); } +ScrStr *scr_insp_dyn_format_s(ScrDyn *d, double depth) { + /* hasBuiltInToString is false for MIMEType and MIMEParams (their + * constructors are not globals): %s prints String(value). */ + if (d->kind == SCR_DYN_HANDLE && (d->v.handle.tag == SCR_DYNH_MIME_TYPE || d->v.handle.tag == SCR_DYNH_MIME_PARAMS)) + return scr_dyn_string_coerce_js(d); + return scr_insp_dyn_s(d, depth); +} + +/* ── util.format over checked-dynamic values ───────────────────────────── + * formatWithOptionsInternal over argument values whose types are known + * only at run time: util.format and util.formatWithOptions with a runtime + * format string or spread arguments, console output with a runtime format + * string, and debuglog's lines. The %-substitutions (%s %d %i %f %j %o %O + * %c %%) follow Node's argument cursor, then the remaining arguments join + * with spaces, strings verbatim and everything else inspected under the + * options. With colors, primitives take inspect's default styles; colored + * object renderings have no lowering. These entries call program code + * (toString, valueOf and toJSON), so unlike the rest of this file they + * can throw: NULL with the exception pending. */ +static void insp_fmt_fence(const ScrInspFormatOptions *o, const char *what) { + InspBuf b = {0}; + ib_cstr(&b, o->surface); + ib_char(&b, ' '); + ib_cstr(&b, what); + ScrStr *message = ib_take(&b); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); + scr_str_release(message); +} + +static bool insp_fmt_object(const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_OBJ: + case SCR_DYN_ARR: + case SCR_DYN_FUNC: + case SCR_DYN_BYTES: + case SCR_DYN_HANDLE: + case SCR_DYN_PROMISE: + case SCR_DYN_TYPED_REF: + case SCR_DYN_PROXY: + return true; + default: + return false; + } +} + +void scr_insp_format_defaults(ScrInspFormatOptions *o, const char *surface) { + o->surface = surface; + o->depth = 2; + o->depth_kind = 0; + o->depth_number = 2; + o->colors = false; + o->custom_inspect = true; + o->break_length = 80; + o->compact = 3; + o->sorted = false; + o->user_options = NULL; +} + +/* inspect(value, options) at the given depth, compact and colors, with + * the options' breakLength and sorted; the previous layout returns + * afterwards. +1, or NULL with the exception pending. */ +static ScrStr *insp_fmt_inspect(ScrDyn *value, const ScrInspFormatOptions *o, double depth, + double compact, bool colors) { + double break_length = g_break_length, saved_compact = g_compact; + bool sorted = g_sorted, saved_colors = g_colors, saved_custom = g_custom_inspect; + int saved_depth_kind = g_depth_kind; + double saved_depth_number = g_depth_number; + const ScrDyn *saved_user = g_user_options; + scr_insp_layout(o->break_length, compact, o->sorted); + g_colors = colors; + g_custom_inspect = o->custom_inspect; + g_depth_kind = o->depth_kind; + g_depth_number = o->depth_number; + g_user_options = o->user_options; + ScrStr *text = scr_insp_dyn(value, 0, depth); + g_colors = saved_colors; + g_custom_inspect = saved_custom; + g_depth_kind = saved_depth_kind; + g_depth_number = saved_depth_number; + g_user_options = saved_user; + scr_insp_layout(break_length, saved_compact, sorted); + if (scr_exc_pending()) { + scr_str_release(text); + return NULL; + } + return text; +} + +/* hasBuiltInToString's negative answer: a toString the program defined on + * the object or its prototype chain (1). Symbol-keyed members (where + * Symbol.toPrimitive would also count) are reported as unknown (-1), and a + * class instance whose class defines toString as -2: its methods are + * compiled for the class's own calls, which this runtime cannot make. */ +static int insp_fmt_custom_to_string(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrStr *to_string = scr_str_new("toString", 8); + bool has = scr_dyn_typed_ref_class_has(value, to_string); + scr_str_release(to_string); + return has ? -2 : 0; + } + for (const ScrDyn *current = value; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { + if (current->symbol_properties) return -1; + const ScrDyn *member = scr_dyn_obj_get(current, "toString", 8); + if (member) return member->kind == SCR_DYN_FUNC; + } + return 0; +} + +static ScrStr *insp_fmt_s(ScrDyn *value, const ScrInspFormatOptions *o) { + if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); + if (value->kind == SCR_DYN_NUM) return scr_insp_f64(value->v.num); + if (value->kind == SCR_DYN_BIGINT) return scr_bigint_inspect(value->v.bigint); + if (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_MIME_TYPE || value->v.handle.tag == SCR_DYNH_MIME_PARAMS)) + return scr_dyn_string_coerce_js(value); + if (value->kind == SCR_DYN_FUNC) { + insp_fmt_fence(o, "%s of a function prints its source text, which a compiled binary does not keep"); + return NULL; + } + if ((value->kind == SCR_DYN_OBJ && scr_dyn_obj_get(value, "%error", 6) == NULL) || + value->kind == SCR_DYN_TYPED_REF) { + int custom = insp_fmt_custom_to_string(value); + if (custom == -2) { + insp_fmt_fence(o, "%s of a class instance whose class defines toString has no lowering"); + return NULL; + } + if (custom < 0) { + insp_fmt_fence(o, "%s of objects with symbol-keyed members has no lowering"); + return NULL; + } + if (custom) return scr_dyn_string_coerce_js(value); + } + if (!insp_fmt_object(value)) return scr_insp_dyn(value, 0, 0); + /* inspect(arg, { ...inspectOptions, depth: 0, colors: false, compact: 3 }) */ + return insp_fmt_inspect(value, o, 0, 3, false); +} + +static ScrStr *insp_fmt_number(ScrDyn *value, int spec) { + if (value->kind == SCR_DYN_BIGINT && spec != 'f') return scr_bigint_inspect(value->v.bigint); + if (value->kind == SCR_DYN_SYMBOL) return scr_str_new("NaN", 3); + double number; + if (spec == 'd') { + if (!scr_dyn_number_coerce_js(value, &number)) return NULL; + } else { + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return NULL; + number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); + scr_str_release(text); + } + return scr_insp_f64(number); +} + +/* One formatter's conversions: %s, %j, %d/%i/%f, %O/%o and the remaining + * arguments' inspect (depth -1: the options' depth). The loop references + * only the table it runs, so a program links only the conversions it uses. */ +typedef struct { + ScrStr *(*s)(ScrDyn *value, const ScrInspFormatOptions *o); + ScrStr *(*j)(const ScrDyn *value); + ScrStr *(*number)(ScrDyn *value, int spec); + ScrStr *(*inspect)(ScrDyn *value, const ScrInspFormatOptions *o, double depth); +} InspFmtOps; + +static ScrStr *insp_format_loop(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o, + const InspFmtOps *ops) { + if (argc == 0) return scr_str_new("", 0); + InspBuf out = {0}; + size_t a = 0; + const char *join = ""; + if (args[0]->kind == SCR_DYN_STR) { + const ScrStr *first = args[0]->v.str; + if (argc == 1) return scr_str_retain((ScrStr *)first); + const char *text = first->data; + size_t last = 0; + for (size_t i = 0; first->len > 0 && i < first->len - 1; i++) { + if (text[i] != '%') continue; + int next = (unsigned char)text[++i]; + if (a + 1 != argc) { + ScrStr *piece = NULL; + switch (next) { + case 's': piece = ops->s(args[++a], o); break; + case 'j': piece = ops->j(args[++a]); break; + case 'd': + case 'i': + case 'f': piece = ops->number(args[++a], next); break; + case 'O': piece = ops->inspect(args[++a], o, o->depth); break; + case 'o': + if (insp_fmt_object(args[a + 1])) { + insp_fmt_fence(o, "%o of objects renders hidden properties, which have no lowering; use %O"); + break; + } + piece = ops->inspect(args[++a], o, 4); + break; + case 'c': + a += 1; + piece = scr_str_new("", 0); + break; + case '%': + ib_bytes(&out, text + last, i - last); + last = i + 1; + continue; + default: + continue; + } + if (!piece) { + free(out.data); + return NULL; + } + if (last != i - 1) ib_bytes(&out, text + last, i - 1 - last); + ib_str(&out, piece); + scr_str_release(piece); + last = i + 1; + } else if (next == '%') { + ib_bytes(&out, text + last, i - last); + last = i + 1; + } + } + if (last != 0) { + a++; + join = " "; + if (last < first->len) ib_bytes(&out, text + last, first->len - last); + } + } + for (; a < argc; a++) { + ib_cstr(&out, join); + if (args[a]->kind == SCR_DYN_STR) { + ib_str(&out, args[a]->v.str); + } else { + ScrStr *piece = ops->inspect(args[a], o, o->depth); + if (!piece) { + free(out.data); + return NULL; + } + ib_str(&out, piece); + scr_str_release(piece); + } + join = " "; + } + return ib_take(&out); +} + + +static ScrStr *insp_fmt_inspect_full(ScrDyn *value, const ScrInspFormatOptions *o, double depth) { + return insp_fmt_inspect(value, o, depth, o->compact, o->colors); +} + +/* The conversion tables are filled where they run: a constant table is a + * global, and sanitized builds keep instrumented globals (and the + * conversions they point at) through --gc-sections. */ +ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o) { + InspFmtOps ops; + ops.s = insp_fmt_s; + ops.j = scr_util_format_j; + ops.number = insp_fmt_number; + ops.inspect = insp_fmt_inspect_full; + return insp_format_loop(args, argc, o, &ops); +} + +/* The formatter over arguments that are all strings, numbers, booleans, + * null or undefined (the compiler proves it from their types): the same + * rules for those kinds, without the checked-dynamic walk the other + * values need. */ +static ScrStr *insp_prim_to_string(const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_STR: return scr_str_retain(v->v.str); + case SCR_DYN_NUM: { + char buf[40]; + size_t n = scr_f64_to_str(v->v.num, buf); + return scr_str_new(buf, n); + } + case SCR_DYN_BOOL: return v->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); + case SCR_DYN_NULL: return scr_str_new("null", 4); + default: return scr_str_new("undefined", 9); + } +} + +static ScrStr *insp_prim_inspect(ScrDyn *v, const ScrInspFormatOptions *o, double depth) { + (void)o; + (void)depth; + if (v->kind == SCR_DYN_STR) return scr_insp_str(v->v.str); + if (v->kind == SCR_DYN_NUM) return scr_insp_f64(v->v.num); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_s(ScrDyn *v, const ScrInspFormatOptions *o) { + (void)o; + if (v->kind == SCR_DYN_NUM) return scr_insp_f64(v->v.num); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_j(const ScrDyn *v) { + if (v->kind == SCR_DYN_STR) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_json_str(&b, v->v.str); + return scr_jb_finish(&b); + } + if (v->kind == SCR_DYN_NUM && !isfinite(v->v.num)) return scr_str_new("null", 4); + return insp_prim_to_string(v); +} + +static ScrStr *insp_prim_number(ScrDyn *v, int spec) { + double number; + if (spec == 'd') { + number = v->kind == SCR_DYN_NUM ? v->v.num + : v->kind == SCR_DYN_STR ? scr_string_to_number(v->v.str) + : v->kind == SCR_DYN_BOOL ? (v->v.b ? 1 : 0) + : v->kind == SCR_DYN_NULL ? 0 + : NAN; + } else { + ScrStr *text = insp_prim_to_string(v); + number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); + scr_str_release(text); + } + return scr_insp_f64(number); +} + +/* A native list of checked values (the compiled format calls) as the + * runtime's argument pack. +1. */ +static ScrDyn *insp_list_pack(ScrArr *list) { + ScrDyn *pack = scr_dyn_new_arr(); + for (uint32_t i = 0; i < list->len; i++) scr_dyn_arr_push(pack, scr_arr_get_ref(list, i)); + return pack; +} + +ScrStr *scr_insp_format_primitives(ScrArr *list) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.format"); + ScrDyn *pack = insp_list_pack(list); + InspFmtOps ops; + ops.s = insp_prim_s; + ops.j = insp_prim_j; + ops.number = insp_prim_number; + ops.inspect = insp_prim_inspect; + ScrStr *out = insp_format_loop(pack->v.arr.items, pack->v.arr.len, &o, &ops); + scr_dyn_release(pack); + return out; +} + +/* The arguments of a dyn array pack (+1 text, or NULL pending). */ +static ScrStr *insp_fmt_pack(ScrDyn *pack, const ScrInspFormatOptions *o) { + if (pack->kind != SCR_DYN_ARR) return scr_insp_format(NULL, 0, o); + return scr_insp_format(pack->v.arr.items, pack->v.arr.len, o); +} + +ScrStr *scr_insp_format_pack(ScrDyn *pack) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.format"); + return insp_fmt_pack(pack, &o); +} + +ScrStr *scr_insp_format_list(ScrArr *list) { + ScrDyn *pack = insp_list_pack(list); + ScrStr *out = scr_insp_format_pack(pack); + scr_dyn_release(pack); + return out; +} + +/* Whether a format string can substitute: a '%' before its last character. + * A string without one formats as its arguments joined with spaces. */ +bool scr_insp_format_specifies(ScrStr *first) { + return first->len > 1 && memchr(first->data, '%', first->len - 1) != NULL; +} + +bool scr_insp_format_specifies_dyn(ScrDyn *first) { + return first->kind == SCR_DYN_STR && scr_insp_format_specifies(first->v.str); +} + +/* One own enumerable option of an inspect options object: +1 value, a + * borrowed undefined when absent, or NULL with the exception pending. */ +static ScrDyn *insp_opt(const ScrDyn *options, const char *key, bool *present) { + size_t len = strlen(key); + *present = options->kind == SCR_DYN_OBJ && scr_dyn_obj_get(options, key, len) != NULL && + scr_dyn_obj_enumerable(options, key, len); + if (!*present) return scr_dyn_retain(scr_dyn_undefined()); + return scr_dyn_obj_read(options, key, len); +} + +/* ctx.depth from an option or inspect's third argument: a number, null + * or undefined (no limit for null, undefined and NaN). */ +static bool insp_depth_option(const ScrDyn *value, ScrInspFormatOptions *o) { + if (value->kind == SCR_DYN_NUM) { + o->depth = isnan(value->v.num) ? INFINITY : value->v.num; + o->depth_kind = 0; + o->depth_number = value->v.num; + } else if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) { + o->depth = INFINITY; + o->depth_kind = value->kind == SCR_DYN_NULL ? 1 : 2; + } else { + return false; + } + return true; +} + +/* inspect's option keys (inspectDefaultOptions and stylize) over an + * options object, as `inspect(value, options)` copies them. Options at + * their defaults, depth, colors, compact (a number or false), + * breakLength (a number) and sorted (a boolean) lower; other values of + * the known options throw the SC2020 refusal. */ +bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o) { + ScrDyn *view = NULL; + if (options->kind == SCR_DYN_TYPED_REF) { + view = scr_dyn_typed_ref_materialize(options); + if (scr_exc_pending()) { + scr_dyn_release(view); + return false; + } + options = view; + } + bool ok = false, present; + ScrDyn *value = NULL; +#define INSP_OPT(key) \ + do { \ + scr_dyn_release(value); \ + value = insp_opt(options, key, &present); \ + if (!value) goto done; \ + } while (0) + INSP_OPT("depth"); + if (present) { + if (!insp_depth_option(value, o)) { + insp_fmt_fence(o, "with a depth option that is not a number or null has no lowering"); + goto done; + } + } + INSP_OPT("colors"); + if (present) o->colors = scr_dyn_truthy(value); + INSP_OPT("customInspect"); + if (present) o->custom_inspect = scr_dyn_truthy(value); + INSP_OPT("compact"); + if (present) { + if (value->kind == SCR_DYN_NUM) o->compact = value->v.num < 0 ? 0 : value->v.num; + else if (value->kind == SCR_DYN_BOOL && !value->v.b) o->compact = -1; + else { + insp_fmt_fence(o, "with this compact option has no lowering (compact must be false or a number)"); + goto done; + } + } + INSP_OPT("breakLength"); + if (present) { + if (value->kind != SCR_DYN_NUM) { + insp_fmt_fence(o, "with a breakLength option that is not a number has no lowering"); + goto done; + } + o->break_length = value->v.num; + } + INSP_OPT("sorted"); + if (present) { + if (value->kind != SCR_DYN_BOOL) { + insp_fmt_fence(o, "with a sorted option that is not a boolean has no lowering"); + goto done; + } + o->sorted = value->v.b; + } + /* The options whose non-default values change renderings this runtime + * does not produce. */ + static const struct { + const char *key; + int kind; /* 0: falsy, 1: true, 2: the number 100, 3: the number 10000 */ + } fixed[] = { + {"showHidden", 0}, {"showProxy", 0}, {"maxArrayLength", 2}, {"maxStringLength", 3}, + {"getters", 0}, {"numericSeparator", 0}, {"stylize", 0}, + }; + for (size_t i = 0; i < sizeof fixed / sizeof *fixed; i++) { + INSP_OPT(fixed[i].key); + if (!present) continue; + bool at_default = fixed[i].kind == 0 ? !scr_dyn_truthy(value) + : fixed[i].kind == 1 ? value->kind == SCR_DYN_BOOL && value->v.b + : value->kind == SCR_DYN_NUM && value->v.num == (fixed[i].kind == 2 ? 100 : 10000); + /* The stylize function a custom inspect function received (options + * passed back into util.inspect) is the default for its colors. */ + if (!at_default && strcmp(fixed[i].key, "stylize") == 0 && insp_own_stylize(value, o->colors)) + at_default = true; + /* stylize is replaced by the color stylizer when colors is set. */ + if (!at_default && !(o->colors && strcmp(fixed[i].key, "stylize") == 0)) { + InspBuf b = {0}; + ib_cstr(&b, "with the "); + ib_cstr(&b, fixed[i].key); + ib_cstr(&b, " option has no lowering"); + ScrStr *what = ib_take(&b); + insp_fmt_fence(o, what->data); + scr_str_release(what); + goto done; + } + } +#undef INSP_OPT + /* Keys inspect does not read: the custom functions receive them. */ + if (!view && options->kind == SCR_DYN_OBJ) { + static const char *const known[] = { + "showHidden", "depth", "colors", "customInspect", "showProxy", + "maxArrayLength", "maxStringLength", "breakLength", "compact", "sorted", + "getters", "numericSeparator", "stylize", + }; + for (size_t i = 0; i < options->v.obj.len && !o->user_options; i++) { + const ScrDynEntry *ent = &options->v.obj.entries[i]; + if (!ent->enumerable) continue; + bool read = false; + for (size_t k = 0; k < sizeof known / sizeof *known && !read; k++) + read = ent->key_len == strlen(known[k]) && memcmp(ent->key, known[k], ent->key_len) == 0; + if (!read) o->user_options = options; + } + } + ok = true; +done: + scr_dyn_release(value); + scr_dyn_release(view); + return ok; +} + +/* util.inspect(value[, options]) and inspect(value, showHidden, depth, + * colors) over checked-dynamic arguments; `argc` is the call's argument + * count. A third argument other than undefined sets the depth and a + * fourth the colors; a boolean second argument is showHidden, and an + * object supplies the options, which apply after the legacy arguments. + * +1, or NULL with the exception pending. */ +ScrStr *scr_insp_inspect(ScrDyn *value, ScrDyn *options, ScrDyn *depth, ScrDyn *colors, + double argc) { + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.inspect"); + if (argc >= 3 && depth->kind != SCR_DYN_UNDEF) { + if (!insp_depth_option(depth, &o)) { + insp_fmt_fence(&o, "with a depth argument that is not a number or null has no lowering"); + return NULL; + } + } + if (argc >= 4 && colors->kind != SCR_DYN_UNDEF) o.colors = scr_dyn_truthy(colors); + if (options->kind == SCR_DYN_BOOL) { + if (options->v.b) { + insp_fmt_fence(&o, "with showHidden has no lowering"); + return NULL; + } + } else if (options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_TYPED_REF) { + if (!scr_insp_format_read_options(options, &o)) return NULL; + } + return insp_fmt_inspect(value, &o, o.depth, o.compact, o.colors); +} + +static ScrDyn *insp_inspect_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *undefined = scr_dyn_undefined(); + scr_insp_nested_begin(); + ScrStr *text = scr_insp_inspect(argc > 0 ? args[0] : undefined, argc > 1 ? args[1] : undefined, + argc > 2 ? args[2] : undefined, argc > 3 ? args[3] : undefined, + (double)argc); + scr_insp_nested_restore(); + if (!text) return NULL; + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; +} + +/* util.formatWithOptions(inspectOptions, ...pack): validateObject (arrays + * pass, null and functions do not), then the options as inspect reads + * them. */ +ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack) { + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_ARR && + options->kind != SCR_DYN_TYPED_REF) { + scr_dyn_arg_type_fail("inspectOptions", "of type object", options); + return NULL; + } + ScrInspFormatOptions o; + scr_insp_format_defaults(&o, "util.formatWithOptions"); + if (!scr_insp_format_read_options(options, &o)) return NULL; + return insp_fmt_pack(pack, &o); +} + /* util.format's %o over a dyn value: util.inspect with showHidden at the * given depth. */ ScrStr *scr_insp_dyn_hidden(ScrDyn *d, double depth) { diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 8af60f0c0..aac4e6589 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -51,9 +51,58 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth); ScrStr *scr_insp_bytes(ScrBytes *b, double recurse, double depth); /* format's %s / rest-args twin: dyn strings pass verbatim. */ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth); +/* util.format over checked-dynamic arguments (formatWithOptionsInternal): + * the options an inspect of each argument takes, and the API name the + * SC2020 refusals name. These entries call program code, so they answer + * NULL with the exception pending when it throws. */ +typedef struct ScrInspFormatOptions { + const char *surface; + double depth; /* Infinity for null, undefined and NaN */ + /* The depth as the options gave it, which custom inspect functions + * receive when the depth is unbounded: 0 a number (depth_number, + * Infinity or NaN), 1 null, 2 undefined. */ + int depth_kind; + double depth_number; + bool colors; + bool custom_inspect; + double break_length; + double compact; /* -1 for false */ + bool sorted; + /* The options object, when it holds keys inspect does not read: custom + * inspect functions receive them (borrowed; NULL otherwise). */ + const ScrDyn *user_options; +} ScrInspFormatOptions; +void scr_insp_format_defaults(ScrInspFormatOptions *o, const char *surface); +/* Reads inspect's options from an options object into o; false with the + * SC2020 refusal pending for values that have no lowering. */ +bool scr_insp_format_read_options(const ScrDyn *options, ScrInspFormatOptions *o); +ScrStr *scr_insp_format(ScrDyn *const *args, size_t argc, const ScrInspFormatOptions *o); +/* util.format(...pack) and util.formatWithOptions(options, ...pack) over a + * dyn array of the arguments (+1). */ +ScrStr *scr_insp_format_pack(ScrDyn *pack); +/* util.format over a native list of checked values, the compiled calls' + * held arguments (+1). */ +struct ScrArr; +ScrStr *scr_insp_format_list(struct ScrArr *list); +/* scr_insp_format_list when every argument is a string, number, boolean, + * null or undefined, without the checked-dynamic walk (+1). */ +ScrStr *scr_insp_format_primitives(struct ScrArr *list); +ScrStr *scr_insp_format_with_options(ScrDyn *options, ScrDyn *pack); +/* util.inspect(value, options, depth, colors) over checked-dynamic + * arguments with Node's legacy-argument rules; argc is the call's + * argument count (+1, or NULL with the exception pending). */ +ScrStr *scr_insp_inspect(ScrDyn *value, ScrDyn *options, ScrDyn *depth, ScrDyn *colors, + double argc); +/* Whether a format string has a '%' before its last character, the only + * strings whose substitutions can change the output. */ +bool scr_insp_format_specifies(ScrStr *first); +bool scr_insp_format_specifies_dyn(ScrDyn *first); +/* format's %s over a dyn value: scr_insp_dyn_s, except that values whose + * class defines its own toString print String(value). */ +ScrStr *scr_insp_dyn_format_s(ScrDyn *d, double depth); ScrStr *scr_insp_dyn_hidden(ScrDyn *d, double depth); /* %o: showHidden */ void scr_insp_begin(double recurse); -/* util.inspect's breakLength, compact (0 for false) and sorted for the +/* util.inspect's breakLength, compact (-1 for false) and sorted for the * renders that follow; the call site restores the defaults (80, 3, false) * afterwards. */ void scr_insp_layout(double break_length, double compact, bool sorted); @@ -80,6 +129,38 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, * caller's level (+1; borrows both). */ void scr_insp_nested_begin(void); ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner); +/* nested_end's restore without its text: the caller's context returns. */ +void scr_insp_nested_restore(void); +/* ctx.customInspect for the renders that follow; answers the previous + * value for the caller to restore. */ +bool scr_insp_custom_inspect(bool enabled); +/* Around a custom inspect function's call: begin gives the calls it makes + * a fresh inspect context with the default options and answers a token; + * end(token) restores the caller's, also after calls that threw. */ +double scr_insp_custom_call_begin(void); +void scr_insp_custom_call_end(double token); +/* How the unbounded depth of the renders that follow was given, for custom + * inspect functions: 0 Infinity, 1 null, 2 undefined. Answers the + * previous kind for the caller to restore. */ +double scr_insp_depth_kind(double kind); +/* A custom inspect function's string result re-indented at the current + * level (+1; borrows text). */ +ScrStr *scr_insp_custom_text(ScrStr *text); +/* The compiled [util.inspect.custom] calls: ctx.customInspect, the depth + * argument at recursion `recurse` of `depth` and the options object (+1 + * dyn values; options NULL with an exception pending), util.inspect as a + * function value (+1), whether a dyn result is the instance itself, and a + * dyn result's rendering at the instance's level (+1). */ +bool scr_insp_custom_enabled(void); +/* Registers a class's compiled custom inspect function under the type key + * of its instances' typed refs: fn(self, depth, options, inspect) answers + * the method's result (borrows both). */ +void scr_insp_register_custom(ScrStr *type_key, ScrDyn *fn); +ScrDyn *scr_insp_custom_depth(double recurse, double depth); +ScrDyn *scr_insp_custom_options(double depth); +ScrDyn *scr_insp_inspect_function(void); +bool scr_insp_custom_is_self(ScrDyn *result, ScrDyn *self); +ScrStr *scr_insp_custom_result(ScrDyn *result, double recurse, double depth); /* Mark the inspector's state before a top-level render, and return to the * mark when the render ends, normally or by an inspection refusal. */ void scr_insp_mark(void); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index fb62bc7a4..e19ea65cc 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -184,6 +184,24 @@ bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key) { key->v.symbol.value == scr_async_dispose_symbol->v.symbol.value; } +/* Symbol.for('nodejs.util.inspect.custom'), installed by the symbol unit + * when the program creates it: the key util.inspect reads custom inspect + * functions under. */ +static SCR_TL ScrDyn *scr_inspect_custom_symbol; + +static void scr_inspect_custom_symbol_cleanup(void) { + scr_dyn_release(scr_inspect_custom_symbol); + scr_inspect_custom_symbol = NULL; +} + +void scr_dyn_install_inspect_custom_symbol(ScrDyn *key) { + if (scr_inspect_custom_symbol) return; + scr_inspect_custom_symbol = scr_dyn_retain(key); + scr_atexit(scr_inspect_custom_symbol_cleanup); +} + +ScrDyn *scr_dyn_inspect_custom_symbol(void) { return scr_inspect_custom_symbol; } + static void scr_to_primitive_symbol_cleanup(void) { scr_dyn_release(scr_to_primitive_symbol); scr_to_primitive_symbol = NULL; @@ -6797,11 +6815,12 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { owner->props = scr_box_retain(function->v.fn.class_obj->static_data); if (!owner->props) { ScrDyn *table = scr_dyn_new_obj(); + /* V8's own-key order: length, then name. */ + scr_dyn_obj_set(table, "length", 6, scr_dyn_new_num((double)function->v.fn.arity)); const char *name = function->v.fn.name ? function->v.fn.name : ""; ScrStr *name_string = scr_str_new(name, strlen(name)); scr_dyn_obj_set(table, "name", 4, scr_dyn_new_str(name_string)); scr_str_release(name_string); - scr_dyn_obj_set(table, "length", 6, scr_dyn_new_num((double)function->v.fn.arity)); for (size_t i = 0; i < table->v.obj.len; i++) { table->v.obj.entries[i].writable = false; table->v.obj.entries[i].enumerable = false; @@ -10388,7 +10407,8 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { if (scr_dyn_error_encoding(d) && !scr_dyn_property_owner(d, "toString", 8)) return scr_dyn_error_string(d); return scr_str_new("[object Object]", 15); case SCR_DYN_HANDLE: - if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL || d->v.handle.tag == SCR_DYNH_DATE) { + if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL || d->v.handle.tag == SCR_DYNH_DATE || + d->v.handle.tag == SCR_DYNH_MIME_TYPE || d->v.handle.tag == SCR_DYNH_MIME_PARAMS) { ScrDyn *text = scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "toString", NULL, 0, "RegExp.toString"); if (!text) return NULL; ScrStr *out = scr_str_retain(text->v.str); @@ -13823,7 +13843,9 @@ static ScrDyn *scr_json_to_json_at(ScrDyn *value, const ScrStr *key, size_t inde if (scr_exc_pending()) { scr_dyn_release(view); scr_dyn_release(value); return NULL; } if (inspected->kind == SCR_DYN_HANDLE && (inspected->v.handle.tag == SCR_DYNH_DATE || inspected->v.handle.tag == SCR_DYNH_URL || inspected->v.handle.tag == SCR_DYNH_NET_SOCKET_ADDRESS || - inspected->v.handle.tag == SCR_DYNH_NET_BLOCK_LIST)) { + inspected->v.handle.tag == SCR_DYNH_NET_BLOCK_LIST || + inspected->v.handle.tag == SCR_DYNH_MIME_TYPE || + inspected->v.handle.tag == SCR_DYNH_MIME_PARAMS)) { const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(inspected); ScrDyn *converted = ops->invoke(inspected->v.handle.ptr, value, "toJSON", NULL, 0, "toJSON"); scr_dyn_release(value); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index a9fd25915..1a625cdf1 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -11588,8 +11588,8 @@ double scr_date_get_timezone_offset(double ms) { * from local fields reads those fields back. */ /* The host zone's offset (local - UTC) in ms at UTC instant `ms` (an - * integral time value within about ±8.65e15). */ -static bool scr_date_offset_at(double ms, double *out) { + * integral time value within about ±8.65e15), from the host's localtime. */ +static bool scr_date_offset_at_uncached(double ms, double *out) { long long cms = (long long)ms; double secd = (double)(cms / 1000 - (cms % 1000 < 0)); struct tm tmv; @@ -11611,6 +11611,60 @@ static bool scr_date_offset_at(double ms, double *out) { return true; } +/* Offsets at UTC midnights, for scr_date_offset_at. A day whose two + * midnights have the same offset has that offset throughout, given at most + * one transition per day (scr_date_local_to_utc already takes transitions + * to be at least a day apart). Each local setter asks for three to five + * offsets around one time value, and consecutive ones land on the same or + * neighbouring days, so most answers come from here instead of a + * localtime(3) call: glibc takes its zone lock in each and, for dates past + * the zone file's last transition, parses the zone's rule string again. A + * change of process.env.TZ starts a new generation. */ +#define SCR_DATE_MIDNIGHTS 64 +typedef struct { + long long day; + double offset; + bool valid; +} ScrDateMidnight; +typedef struct { + bool valid; + unsigned generation; + ScrDateMidnight entries[SCR_DATE_MIDNIGHTS]; +} ScrDateMidnightCache; +static SCR_TL ScrDateMidnightCache scr_date_midnight_cache; + +static bool scr_date_midnight_offset(ScrDateMidnightCache *c, long long day, double *out) { + ScrDateMidnight *e = &c->entries[(unsigned long long)day % SCR_DATE_MIDNIGHTS]; + if (e->valid && e->day == day) { + *out = e->offset; + return true; + } + if (!scr_date_offset_at_uncached((double)day * 86400000.0, out)) return false; + e->valid = true; + e->day = day; + e->offset = *out; + return true; +} + +static bool scr_date_offset_at(double ms, double *out) { + ScrDateMidnightCache *c = &scr_date_midnight_cache; + unsigned generation = scr_date_tz_generation; + if (!c->valid || c->generation != generation) { + memset(c, 0, sizeof *c); + c->valid = true; + c->generation = generation; + } + long long cms = (long long)ms; + long long day = cms / 86400000 - (cms % 86400000 < 0); + double start, end; + if (scr_date_midnight_offset(c, day, &start) && scr_date_midnight_offset(c, day + 1, &end) && + start == end) { + *out = start; + return true; + } + return scr_date_offset_at_uncached(ms, out); +} + static volatile const double scr_date_day_ms = 86400000.0; static double scr_date_local_to_utc(double t) { diff --git a/packages/runtime/src/scr_mime.c b/packages/runtime/src/scr_mime.c new file mode 100644 index 000000000..352a7d391 --- /dev/null +++ b/packages/runtime/src/scr_mime.c @@ -0,0 +1,603 @@ +/* node:util MIMEType and MIMEParams (Node's lib/internal/mime.js) as + * native handles of the checked-dynamic tree. The parser scans UTF-16 code + * units, as Node's string searches do, so error offsets match; every value + * that survives validation is ASCII (types, subtypes, parameter names) or + * Latin-1 (parameter values). MIMEParams keeps Map semantics: insertion + * order, a set of an existing name keeps its place, and a deleted entry + * leaves a gap that live iterators skip. A separate unit, linked only by + * programs that construct these classes. */ +#include "scr_runtime.h" + +#include +#include +#include + +typedef struct { + size_t rc; + ScrStr **keys; + ScrStr **values; /* NULL for a deleted entry */ + size_t len, cap; +} ScrMimeParams; + +typedef struct { + size_t rc; + ScrStr *type; + ScrStr *subtype; + ScrDyn *params; /* the MIMEParams handle box, owned */ +} ScrMimeType; + +static void scr_mime_oom(void) { scr_trap("scriptc: out of memory\n"); } + +/* ── code units ─────────────────────────────────────────────────────── */ + +typedef struct { + uint16_t *units; + size_t len; +} ScrMimeUnits; + +static ScrMimeUnits scr_mime_units(ScrStr *text) { + ScrMimeUnits out; + out.len = (size_t)scr_str_utf16_len(text); + out.units = malloc((out.len ? out.len : 1) * sizeof *out.units); + if (!out.units) scr_mime_oom(); + for (size_t i = 0; i < out.len; i++) out.units[i] = (uint16_t)scr_str_char_code_at(text, (double)i); + return out; +} + +static bool scr_mime_ws(uint16_t c) { return c == '\r' || c == '\n' || c == '\t' || c == ' '; } + +static bool scr_mime_token(uint16_t c) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) return true; + return c != 0 && c < 0x80 && strchr("!#$%&'*+-.^_`|~", (char)c) != NULL; +} + +static bool scr_mime_quoted(uint16_t c) { + return c == '\t' || (c >= 0x20 && c <= 0x7E) || (c >= 0x80 && c <= 0xFF); +} + +/* The index of the first unit of [from, to) outside the token set, as an + * offset from `from`, or -1. */ +static long scr_mime_invalid_token(const uint16_t *u, size_t from, size_t to) { + for (size_t i = from; i < to; i++) + if (!scr_mime_token(u[i])) return (long)(i - from); + return -1; +} + +/* A string of units that are all at most 0xFF, lowered (ASCII only) when + * `lower`. +1. */ +static ScrStr *scr_mime_latin1(const uint16_t *u, size_t from, size_t to, bool lower) { + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = from; i < to; i++) { + uint16_t c = u[i]; + if (lower && c >= 'A' && c <= 'Z') c = (uint16_t)(c + ('a' - 'A')); + if (c < 0x80) { + scr_jb_putc(&b, (char)c); + } else { + char pair[2] = {(char)(0xC0 | (c >> 6)), (char)(0x80 | (c & 0x3F))}; + scr_jb_write(&b, pair, 2); + } + } + return scr_jb_finish(&b); +} + +/* ERR_INVALID_MIME_SYNTAX(production, str, invalidIndex). */ +static void scr_mime_syntax_fail(const char *production, const ScrStr *str, long index) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "The MIME syntax for a "); + scr_jb_puts(&b, production); + scr_jb_puts(&b, " in \""); + scr_jb_put_str(&b, str); + scr_jb_puts(&b, "\" is invalid"); + if (index != -1) { + char at[32]; + snprintf(at, sizeof at, " at %ld", index); + scr_jb_puts(&b, at); + } + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_MIME_SYNTAX"); + scr_str_release(message); +} + +/* ── MIMEParams ─────────────────────────────────────────────────────── */ + +static ScrMimeParams *scr_mime_params_alloc(void) { + ScrMimeParams *p = calloc(1, sizeof *p); + if (!p) scr_mime_oom(); + p->rc = 1; + return p; +} + +static void *scr_mime_params_retain(void *h) { + ((ScrMimeParams *)h)->rc++; + return h; +} + +static void scr_mime_params_release(void *h) { + ScrMimeParams *p = h; + if (!p || --p->rc > 0) return; + for (size_t i = 0; i < p->len; i++) { + scr_str_release(p->keys[i]); + scr_str_release(p->values[i]); + } + free(p->keys); + free(p->values); + free(p); +} + +static long scr_mime_params_find(const ScrMimeParams *p, const ScrStr *key) { + for (size_t i = 0; i < p->len; i++) + if (p->values[i] && scr_str_eq((ScrStr *)p->keys[i], (ScrStr *)key)) return (long)i; + return -1; +} + +/* Map.prototype.set: an existing name keeps its place. Both borrowed. */ +static void scr_mime_params_put(ScrMimeParams *p, ScrStr *key, ScrStr *value) { + long at = scr_mime_params_find(p, key); + if (at >= 0) { + scr_str_release(p->values[at]); + p->values[at] = scr_str_retain(value); + return; + } + if (p->len == p->cap) { + p->cap = p->cap ? p->cap * 2 : 4; + p->keys = realloc(p->keys, p->cap * sizeof *p->keys); + p->values = realloc(p->values, p->cap * sizeof *p->values); + if (!p->keys || !p->values) scr_mime_oom(); + } + p->keys[p->len] = scr_str_retain(key); + p->values[p->len] = scr_str_retain(value); + p->len++; +} + +/* Node's MIMEParams#parse over the text after the subtype's ';'. */ +static void scr_mime_params_parse(ScrMimeParams *p, const uint16_t *u, size_t n) { + size_t end = n; + while (end > 0 && scr_mime_ws(u[end - 1])) end--; + size_t pos = 0; + while (pos < end) { + while (pos < n && scr_mime_ws(u[pos])) pos++; + size_t name_end = pos; + while (name_end < n && u[name_end] != ';' && u[name_end] != '=') name_end++; + size_t name_from = pos; + pos = name_end; + if (pos < end) { + uint16_t c = u[pos]; + pos++; + if (c == ';') continue; + } + if (pos >= end) break; + uint16_t *value = NULL; + size_t value_len = 0; + if (u[pos] == '"') { + pos++; + /* QUOTED_VALUE_PATTERN: escaped units, a trailing backslash (group 1) + * and the closing quote (group 2). */ + size_t i = pos; + bool trailing_backslash = false, closed = false; + while (i < n) { + if (u[i] == '\\') { + if (i + 1 == n) { + trailing_backslash = true; + i++; + break; + } + bool pair = u[i + 1] >= 0xD800 && u[i + 1] <= 0xDBFF && i + 2 < n && u[i + 2] >= 0xDC00 && u[i + 2] <= 0xDFFF; + i += pair ? 3 : 2; + } else if (u[i] != '"') { + i++; + } else { + break; + } + } + if (i < n && u[i] == '"' && !trailing_backslash) { + closed = true; + i++; + } + size_t match = i - pos; + size_t inside = trailing_backslash || closed ? match - 1 : match; + value = malloc((inside + 2) * sizeof *value); + if (!value) scr_mime_oom(); + /* removeBackslashes, then the unmatched backslash back at the end. */ + size_t k; + for (k = 0; inside > 0 && k < inside - 1; k++) { + uint16_t c = u[pos + k]; + if (c == '\\') { + k++; + value[value_len++] = u[pos + k]; + } else { + value[value_len++] = c; + } + } + if (inside > 0 && k == inside - 1) value[value_len++] = u[pos + k]; + if (trailing_backslash) value[value_len++] = '\\'; + pos += match; + } else { + size_t value_end = pos; + while (value_end < n && u[value_end] != ';') value_end++; + size_t trimmed = value_end; + while (trimmed > pos && scr_mime_ws(u[trimmed - 1])) trimmed--; + size_t raw = value_end - pos; + if (trimmed == pos) { + pos += raw; + continue; + } + value_len = trimmed - pos; + value = malloc(value_len * sizeof *value); + if (!value) scr_mime_oom(); + memcpy(value, u + pos, value_len * sizeof *value); + pos += raw; + } + bool valid_name = name_end > name_from && scr_mime_invalid_token(u, name_from, name_end) == -1; + bool valid_value = true; + for (size_t i = 0; i < value_len && valid_value; i++) valid_value = scr_mime_quoted(value[i]); + if (valid_name && valid_value) { + ScrStr *key = scr_mime_latin1(u, name_from, name_end, true); + if (scr_mime_params_find(p, key) < 0) { + ScrStr *text = scr_mime_latin1(value, 0, value_len, false); + scr_mime_params_put(p, key, text); + scr_str_release(text); + } + scr_str_release(key); + } + free(value); + pos++; + } +} + +/* The `encode` step of MIMEParams#toString. */ +static void scr_mime_encode_value(ScrJsonBuf *b, const ScrStr *value) { + if (value->len == 0) { + scr_jb_puts(b, "\"\""); + return; + } + bool plain = true; + for (size_t i = 0; i < value->len && plain; i++) { + unsigned char c = (unsigned char)value->data[i]; + plain = c < 0x80 && scr_mime_token(c); + } + if (plain) { + scr_jb_put_str(b, value); + return; + } + scr_jb_putc(b, '"'); + for (size_t i = 0; i < value->len; i++) { + char c = value->data[i]; + if (c == '"' || c == '\\') scr_jb_putc(b, '\\'); + scr_jb_putc(b, c); + } + scr_jb_putc(b, '"'); +} + +static ScrStr *scr_mime_params_to_string(const ScrMimeParams *p) { + ScrJsonBuf b; + scr_jb_init(&b); + bool first = true; + for (size_t i = 0; i < p->len; i++) { + if (!p->values[i]) continue; + if (!first) scr_jb_putc(&b, ';'); + first = false; + scr_jb_put_str(&b, p->keys[i]); + scr_jb_putc(&b, '='); + scr_mime_encode_value(&b, p->values[i]); + } + return scr_jb_finish(&b); +} + +/* ToString(name), lowered: `toASCIILower(`${name}`)`. +1 or NULL pending. */ +static ScrStr *scr_mime_name_arg(ScrDyn *const *args, size_t argc) { + ScrStr *raw = scr_dyn_string_coerce_js(argc ? args[0] : scr_dyn_undefined()); + if (!raw) return NULL; + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = 0; i < raw->len; i++) { + char c = raw->data[i]; + scr_jb_putc(&b, c >= 'A' && c <= 'Z' ? (char)(c + ('a' - 'A')) : c); + } + scr_str_release(raw); + return scr_jb_finish(&b); +} + +static ScrDyn *scr_mime_not_a_function(const char *what) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; +} + +static ScrDyn *scr_mime_params_step(void *h, double *index, unsigned selection) { + ScrMimeParams *p = h; + size_t i = (size_t)*index; + while (i < p->len && !p->values[i]) i++; + bool done = i >= p->len; + ScrDyn *value; + if (done) { + value = scr_dyn_retain(scr_dyn_undefined()); + *index = (double)p->len; + } else { + if (selection == 2) { + value = scr_dyn_new_arr(); + scr_dyn_arr_push(value, scr_dyn_new_str(p->keys[i])); + scr_dyn_arr_push(value, scr_dyn_new_str(p->values[i])); + } else { + value = scr_dyn_new_str(selection == 1 ? p->keys[i] : p->values[i]); + } + *index = (double)(i + 1); + } + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, value); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(done)); + return result; +} + +static ScrDyn *scr_mime_params_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrMimeParams *p = h; + if (!strcmp(method, "entries") || !strcmp(method, "keys") || !strcmp(method, "values")) + return scr_dyn_native_handle_iterator(self, !strcmp(method, "entries") ? 2 : !strcmp(method, "keys") ? 1 : 0); + if (!strcmp(method, "toString") || !strcmp(method, "toJSON")) { + ScrStr *text = scr_mime_params_to_string(p); + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; + } + bool get = !strcmp(method, "get"), has = !strcmp(method, "has"), del = !strcmp(method, "delete"); + bool set = !strcmp(method, "set"); + if (!get && !has && !del && !set) return scr_mime_not_a_function(what); + ScrStr *name = scr_mime_name_arg(args, argc); + if (!name) return NULL; + ScrDyn *result = NULL; + long at = scr_mime_params_find(p, name); + if (get) { + result = at >= 0 ? scr_dyn_new_str(p->values[at]) : scr_dyn_new_null(); + } else if (has) { + result = scr_dyn_new_bool(at >= 0); + } else if (del) { + if (at >= 0) { + scr_str_release(p->values[at]); + p->values[at] = NULL; + } + result = scr_dyn_retain(scr_dyn_undefined()); + } else { + ScrStr *value = scr_dyn_string_coerce_js(argc > 1 ? args[1] : scr_dyn_undefined()); + if (value) { + ScrMimeUnits nu = scr_mime_units(name); + long bad_name = nu.len == 0 ? -1 : scr_mime_invalid_token(nu.units, 0, nu.len); + free(nu.units); + ScrMimeUnits vu = scr_mime_units(value); + long bad_value = -1; + for (size_t i = 0; i < vu.len && bad_value == -1; i++) + if (!scr_mime_quoted(vu.units[i])) bad_value = (long)i; + free(vu.units); + if (name->len == 0 || bad_name != -1) { + scr_mime_syntax_fail("parameter name", name, bad_name); + } else if (bad_value != -1) { + scr_mime_syntax_fail("parameter value", value, bad_value); + } else { + scr_mime_params_put(p, name, value); + result = scr_dyn_retain(scr_dyn_undefined()); + } + scr_str_release(value); + } + } + scr_str_release(name); + return result; +} + +static ScrDyn *scr_mime_empty_view(void *h) { + (void)h; + return scr_dyn_new_obj(); +} + +/* A method read as a value (`params.get` without a call) has no dyn + * function value here: it throws the "not supported yet" Error; other + * names answer undefined. */ +static ScrDyn *scr_mime_member_get(const char *cls, const char *const *methods, const char *key, size_t len) { + for (size_t i = 0; methods[i]; i++) { + if (strlen(methods[i]) != len || memcmp(methods[i], key, len) != 0) continue; + char message[128]; + int n = snprintf(message, sizeof message, "'%s.prototype.%s' as a value is not supported yet", cls, methods[i]); + scr_throw_error_msg(SCR_ERR_ERROR, message, (size_t)n); + return NULL; + } + return NULL; +} + +static ScrDyn *scr_mime_params_get(void *h, const char *key, size_t len) { + (void)h; + static const char *const methods[] = {"delete", "entries", "get", "has", "keys", "set", "values", "toString", "toJSON", NULL}; + return scr_mime_member_get("MIMEParams", methods, key, len); +} + +static ScrDyn *scr_mime_params_box(ScrMimeParams *p) { + static const ScrDynHandleOps ops = { + .cls = "MIMEParams", + .retain = scr_mime_params_retain, + .release = scr_mime_params_release, + .invoke = scr_mime_params_invoke, + .get = scr_mime_params_get, + .iter_step = scr_mime_params_step, + .inspect_view = scr_mime_empty_view, + .inspect_view_empty = true, + }; + scr_dyn_handle_install(SCR_DYNH_MIME_PARAMS, &ops); + ScrDyn *box = scr_dyn_new_handle(p, SCR_DYNH_MIME_PARAMS); + scr_mime_params_release(p); + return box; +} + +/* new util.MIMEParams(): an empty parameter map. +1. */ +ScrDyn *scr_mime_params_new(void) { return scr_mime_params_box(scr_mime_params_alloc()); } + +/* ── MIMEType ───────────────────────────────────────────────────────── */ + +static void *scr_mime_type_retain(void *h) { + ((ScrMimeType *)h)->rc++; + return h; +} + +static void scr_mime_type_release(void *h) { + ScrMimeType *t = h; + if (!t || --t->rc > 0) return; + scr_str_release(t->type); + scr_str_release(t->subtype); + scr_dyn_release(t->params); + free(t); +} + +/* The setters' validation: a non-empty token, lowered. +1 or NULL pending. */ +static ScrStr *scr_mime_part(const char *production, const ScrDyn *value) { + ScrStr *text = scr_dyn_string_coerce_js(value); + if (!text) return NULL; + ScrMimeUnits u = scr_mime_units(text); + long bad = scr_mime_invalid_token(u.units, 0, u.len); + ScrStr *out = NULL; + if (u.len == 0 || bad != -1) scr_mime_syntax_fail(production, text, bad); + else out = scr_mime_latin1(u.units, 0, u.len, true); + free(u.units); + scr_str_release(text); + return out; +} + +static ScrStr *scr_mime_type_to_string(const ScrMimeType *t) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_str(&b, t->type); + scr_jb_putc(&b, '/'); + scr_jb_put_str(&b, t->subtype); + ScrStr *params = scr_mime_params_to_string(t->params->v.handle.ptr); + if (params->len) { + scr_jb_putc(&b, ';'); + scr_jb_put_str(&b, params); + } + scr_str_release(params); + return scr_jb_finish(&b); +} + +static ScrDyn *scr_mime_type_get(void *h, const char *key, size_t len) { + ScrMimeType *t = h; + if (len == 4 && !memcmp(key, "type", 4)) return scr_dyn_new_str(t->type); + if (len == 7 && !memcmp(key, "subtype", 7)) return scr_dyn_new_str(t->subtype); + if (len == 6 && !memcmp(key, "params", 6)) return scr_dyn_retain(t->params); + if (len == 7 && !memcmp(key, "essence", 7)) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_put_str(&b, t->type); + scr_jb_putc(&b, '/'); + scr_jb_put_str(&b, t->subtype); + ScrStr *text = scr_jb_finish(&b); + ScrDyn *out = scr_dyn_new_str(text); + scr_str_release(text); + return out; + } + static const char *const methods[] = {"toString", "toJSON", NULL}; + return scr_mime_member_get("MIMEType", methods, key, len); +} + +static bool scr_mime_type_set(void *h, const char *key, size_t len, const ScrDyn *value) { + ScrMimeType *t = h; + bool type = len == 4 && !memcmp(key, "type", 4), subtype = len == 7 && !memcmp(key, "subtype", 7); + if (!type && !subtype) return false; + ScrStr *part = scr_mime_part(type ? "type" : "subtype", value); + if (!part) return true; + ScrStr **slot = type ? &t->type : &t->subtype; + scr_str_release(*slot); + *slot = part; + return true; +} + +static ScrDyn *scr_mime_type_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + (void)self; (void)args; (void)argc; + if (strcmp(method, "toString") && strcmp(method, "toJSON")) return scr_mime_not_a_function(what); + ScrStr *text = scr_mime_type_to_string(h); + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; +} + +/* new MIMEType(string, noThrow) and MIMEType.parse(string): NULL with the + * error pending, or (parse) NULL with nothing pending for invalid input. */ +static ScrDyn *scr_mime_type_make(const ScrDyn *input, bool no_throw) { + ScrStr *text = scr_dyn_string_coerce_js(input); + if (!text) return NULL; + ScrMimeUnits u = scr_mime_units(text); + size_t n = u.len, pos = 0; + while (pos < n && scr_mime_ws(u.units[pos])) pos++; + size_t type_end = pos; + while (type_end < n && u.units[type_end] != '/') type_end++; + bool slash = type_end < n; + long bad_type = scr_mime_invalid_token(u.units, pos, type_end); + ScrDyn *result = NULL; + if (type_end == pos || bad_type != -1 || !slash) { + if (!no_throw) scr_mime_syntax_fail("type", text, bad_type); + goto done; + } + size_t type_from = pos; + pos = type_end + 1; + size_t sub_end = pos; + while (sub_end < n && u.units[sub_end] != ';') sub_end++; + size_t sub_from = pos; + size_t trimmed = sub_end; + while (trimmed > sub_from && scr_mime_ws(u.units[trimmed - 1])) trimmed--; + pos = sub_end < n ? sub_end + 1 : sub_end; + long bad_sub = scr_mime_invalid_token(u.units, sub_from, trimmed); + if (trimmed == sub_from || bad_sub != -1) { + if (!no_throw) scr_mime_syntax_fail("subtype", text, bad_sub); + goto done; + } + ScrMimeType *t = calloc(1, sizeof *t); + if (!t) scr_mime_oom(); + t->rc = 1; + t->type = scr_mime_latin1(u.units, type_from, type_end, true); + t->subtype = scr_mime_latin1(u.units, sub_from, trimmed, true); + ScrMimeParams *p = scr_mime_params_alloc(); + scr_mime_params_parse(p, u.units + pos, n - pos); + t->params = scr_mime_params_box(p); + static const ScrDynHandleOps ops = { + .cls = "MIMEType", + .retain = scr_mime_type_retain, + .release = scr_mime_type_release, + .invoke = scr_mime_type_invoke, + .get = scr_mime_type_get, + .set = scr_mime_type_set, + .inspect_view = scr_mime_empty_view, + .inspect_view_empty = true, + }; + scr_dyn_handle_install(SCR_DYNH_MIME_TYPE, &ops); + result = scr_dyn_new_handle(t, SCR_DYNH_MIME_TYPE); + scr_mime_type_release(t); +done: + free(u.units); + scr_str_release(text); + return result; +} + +/* new util.MIMEType(string[, noThrowSymbol]): a second argument that is not + * null or undefined throws ERR_ILLEGAL_CONSTRUCTOR (Node's internal + * kNoThrow symbol is unreachable from programs). +1 or NULL pending. */ +ScrDyn *scr_mime_type_new(ScrDyn *input, ScrDyn *no_throw) { + if (no_throw->kind != SCR_DYN_UNDEF && no_throw->kind != SCR_DYN_NULL) { + static const char message[] = "Illegal constructor"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_ILLEGAL_CONSTRUCTOR"); + return NULL; + } + return scr_mime_type_make(input, false); +} + +/* MIMEType.parse(string): the MIMEType, or null for invalid input. +1, or + * NULL with a conversion error pending. */ +ScrDyn *scr_mime_type_parse(ScrDyn *input) { + ScrDyn *result = scr_mime_type_make(input, true); + if (result || scr_exc_pending()) return result; + return scr_dyn_new_null(); +} + +/* `value instanceof MIMEType` (kind 0) or `MIMEParams` (1). */ +bool scr_mime_is(const ScrDyn *value, double kind) { + return value->kind == SCR_DYN_HANDLE && + value->v.handle.tag == (kind == 0 ? SCR_DYNH_MIME_TYPE : SCR_DYNH_MIME_PARAMS); +} diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c index 8fa68738d..8bc476045 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -1701,3 +1701,33 @@ ScrDyn *scr_fs_constants_object(void) { scr_atexit(scr_fs_constants_clear); return scr_dyn_retain(o); } + +/* util.convertProcessSignalToExitCode(signalCode): 128 plus the number of + * the os.constants.signals entry, and validateOneOf's + * ERR_INVALID_ARG_VALUE, listing every name, for anything else. NaN with + * the exception pending. */ +double scr_util_signal_exit_code(const ScrDyn *code) { + ScrDyn *signals = scr_dyn_new_obj(); + scr_os_signal_constants(signals); + const ScrDyn *number = + code->kind == SCR_DYN_STR ? scr_dyn_obj_get(signals, code->v.str->data, code->v.str->len) : NULL; + double result = NAN; + if (number && number->kind == SCR_DYN_NUM) { + result = 128 + number->v.num; + } else { + ScrJsonBuf reason; + scr_jb_init(&reason); + scr_jb_puts(&reason, "must be one of: "); + for (size_t i = 0; i < signals->v.obj.len; i++) { + if (i > 0) scr_jb_puts(&reason, ", "); + scr_jb_putc(&reason, '\''); + scr_jb_write(&reason, signals->v.obj.entries[i].key, signals->v.obj.entries[i].key_len); + scr_jb_putc(&reason, '\''); + } + ScrStr *text = scr_jb_finish(&reason); + scr_dyn_arg_value_fail("signalCode", text->data, code); + scr_str_release(text); + } + scr_dyn_release(signals); + return result; +} diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index 57f219ad1..41bee5a8f 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -23,6 +23,9 @@ ScrStr *scr_os_hostname(void); ScrStr *scr_os_endianness(void); double scr_os_freemem(void); double scr_os_uptime(void); +/* util.convertProcessSignalToExitCode: 128 plus the signal number, or NaN + * with ERR_INVALID_ARG_VALUE pending (scr_os.c). */ +double scr_util_signal_exit_code(const struct ScrDyn *code); ScrArr *scr_os_loadavg(void); /* os.getPriority(pid): validateInt32, then the scheduling priority; * failures throw Node's RangeError or SystemError (ERR_SYSTEM_ERROR). */ diff --git a/packages/runtime/src/scr_qs.h b/packages/runtime/src/scr_qs.h index 2232d2d48..1548bfb72 100644 --- a/packages/runtime/src/scr_qs.h +++ b/packages/runtime/src/scr_qs.h @@ -66,6 +66,24 @@ void scr_util_inherits_prototype(struct ScrDyn *ctor, struct ScrDyn *super_ctor, struct ScrDyn *super_prototype); struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code, struct ScrDyn *options); +/* util.isArray and util._extend, with Node's DEP0044 and DEP0060 on the + * first call (scr_util_callable.c). _extend answers target +1. */ +bool scr_util_is_array_deprecated(const struct ScrDyn *value); +struct ScrDyn *scr_util_extend(struct ScrDyn *target, struct ScrDyn *source); +/* util.diff(actual, expected): Node's operation list (+1), or NULL with + * ERR_INVALID_ARG_TYPE pending. */ +struct ScrDyn *scr_util_diff(struct ScrDyn *actual, struct ScrDyn *expected); +/* util.MIMEType and util.MIMEParams (scr_mime.c): +1 handles, or NULL + * with the error pending; parse answers null for invalid input. */ +struct ScrDyn *scr_mime_type_new(struct ScrDyn *input, struct ScrDyn *no_throw); +struct ScrDyn *scr_mime_type_parse(struct ScrDyn *input); +struct ScrDyn *scr_mime_params_new(void); +bool scr_mime_is(const struct ScrDyn *value, double kind); +/* util.debounce(fn, wait, options) and util.throttle(fn, limit, interval, + * options) (scr_util_timing.c): the wrapping function (+1), or NULL with + * the validation error pending. */ +struct ScrDyn *scr_util_debounce(struct ScrDyn *fn, struct ScrDyn *wait, struct ScrDyn *options); +struct ScrDyn *scr_util_throttle(struct ScrDyn *fn, struct ScrDyn *limit, struct ScrDyn *interval, struct ScrDyn *options); struct ScrDyn *scr_util_callbackify(struct ScrDyn *original); struct ScrDyn *scr_util_promisify(struct ScrDyn *original); struct ScrDyn *scr_util_debuglog(struct ScrDyn *section, struct ScrDyn *callback); diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 2a98e92c6..37f94bd65 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -2418,6 +2418,57 @@ static void *scr_normalize_realloc(void *opaque, void *ptr, size_t size) { return realloc(ptr, size); } +/* unicode_normalize, except that U+11A7 never composes. libunicode composes + * an LV syllable with U+11A7 as a trailing consonant (those are U+11A8 to + * U+11C2), which drops it: "\uAC00\u11A7" became "\uAC00". U+11A7 is a + * class-0 vowel that composes with nothing, so for the composing forms the + * code points between occurrences of it normalize on their own and it is + * kept as it is, as in Node. */ +static int scr_unicode_normalize(uint32_t **pdst, const uint32_t *src, int len, + UnicodeNormalizationEnum mode) { + int at = 0; + if (mode == UNICODE_NFC || mode == UNICODE_NFKC) + while (at < len && src[at] != 0x11A7) at++; + else + at = len; + if (at == len) return unicode_normalize(pdst, src, len, mode, NULL, scr_normalize_realloc); + uint32_t *out = NULL; + size_t count = 0, cap = 0; + int start = 0; + for (int i = 0; i <= len; i++) { + if (i < len && src[i] != 0x11A7) continue; + uint32_t *part = NULL; + int n = 0; + if (i > start) { + n = unicode_normalize(&part, src + start, i - start, mode, NULL, scr_normalize_realloc); + if (n < 0) { + free(out); + return -1; + } + } + size_t need = count + (size_t)n + 1; + if (need > cap) { + size_t grown = cap ? cap * 2 : 16; + if (grown < need) grown = need; + uint32_t *bigger = realloc(out, grown * sizeof *out); + if (!bigger) { + free(part); + free(out); + return -1; + } + out = bigger; + cap = grown; + } + if (n > 0) memcpy(out + count, part, (size_t)n * sizeof *out); + count += (size_t)n; + free(part); + if (i < len) out[count++] = 0x11A7; + start = i + 1; + } + *pdst = out; + return (int)count; +} + ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { UnicodeNormalizationEnum mode; if (form->len == 3 && !memcmp(form->data, "NFC", 3)) mode = UNICODE_NFC; @@ -2442,7 +2493,7 @@ ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { offset += step; } uint32_t *output = NULL; - int count = unicode_normalize(&output, input, length, mode, NULL, scr_normalize_realloc); + int count = scr_unicode_normalize(&output, input, length, mode); free(input); if (count < 0) scr_regex_oom(); ScrJsonBuf buffer; @@ -2455,7 +2506,7 @@ ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) { uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len) { if (length > INT32_MAX) scr_regex_oom(); uint32_t *output = NULL; - int count = unicode_normalize(&output, input, (int)length, UNICODE_NFC, NULL, scr_normalize_realloc); + int count = scr_unicode_normalize(&output, input, (int)length, UNICODE_NFC); if (count < 0) scr_regex_oom(); *out_len = (size_t)count; return output; diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index e855a5fb4..7e573fc28 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -150,6 +150,11 @@ ScrSym *scr_sym_for(ScrStr *key) { s->reg_key = scr_str_retain(key); s->reg_next = g_sym_registry; g_sym_registry = scr_sym_retain(s); + if (key->len == 26 && !memcmp(key->data, "nodejs.util.inspect.custom", 26)) { + ScrDyn *custom = scr_dyn_new_symbol(s); + scr_dyn_install_inspect_custom_symbol(custom); + scr_dyn_release(custom); + } return s; } diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index 60de92121..17076b078 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -9,8 +9,8 @@ * and reported on the next tick (scr_warning_emit_deferred), which also * applies process.noDeprecation and process.throwDeprecation. debuglog * matches sections against NODE_DEBUG as the process started and writes - * Node's "SECTION PID: message" lines to stderr. All arguments are - * borrowed. */ + * Node's "SECTION PID: message" lines, formatted by scr_insp_format, to + * stderr. All arguments are borrowed. */ #include "scr_runtime.h" #include @@ -365,6 +365,66 @@ ScrDyn *scr_util_deprecate(ScrDyn *fn, ScrDyn *msg, ScrDyn *code, ScrDyn *option return deprecated; } +/* ── util.isArray and util._extend ───────────────────────────────────── + * Node wraps both with its internal deprecate(): the first call while + * deprecation warnings are on reports DEP0044 or DEP0060 on the next tick, + * unless a util.deprecate wrapper already reported that code. */ +static SCR_TL bool uc_is_array_warned = false; +static SCR_TL bool uc_extend_warned = false; + +static bool uc_internal_deprecation(bool *warned, const char *message, const char *code) { + if (scr_process_deprecation_flag_on(0) || *warned) return true; + *warned = true; + if (!uc_codes_warned) { + uc_codes_warned = scr_dyn_new_obj_null_proto(); + scr_atexit(uc_codes_warned_teardown); + } + size_t code_len = strlen(code); + if (scr_dyn_obj_get(uc_codes_warned, code, code_len)) return true; + ScrStr *text = scr_str_new(message, strlen(message)); + ScrDyn *msg = scr_dyn_new_str(text); + scr_str_release(text); + ScrStr *key = scr_str_new(code, code_len); + bool ok = uc_emit(msg, "DeprecationWarning", key); + if (ok) scr_dyn_obj_set(uc_codes_warned, code, code_len, scr_dyn_new_bool(true)); + scr_str_release(key); + scr_dyn_release(msg); + return ok; +} + +bool scr_util_is_array_deprecated(const ScrDyn *value) { + if (!uc_internal_deprecation(&uc_is_array_warned, + "The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.", + "DEP0044")) + return false; + return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); +} + +/* util._extend(target, source): source's own enumerable string keys, + * assigned to target from the last key to the first; a source that is + * null or not an object leaves target as it is. +1. */ +ScrDyn *scr_util_extend(ScrDyn *target, ScrDyn *source) { + if (!uc_internal_deprecation(&uc_extend_warned, + "The `util._extend` API is deprecated. Please use Object.assign() instead.", + "DEP0060")) + return NULL; + bool object = source->kind == SCR_DYN_OBJ || source->kind == SCR_DYN_ARR || source->kind == SCR_DYN_TYPED_REF || + source->kind == SCR_DYN_HANDLE || source->kind == SCR_DYN_BYTES || source->kind == SCR_DYN_PROMISE || + source->kind == SCR_DYN_PROXY; + if (!object) return scr_dyn_retain(target); + ScrDyn *keys = scr_dyn_obj_keys(source); + if (!keys) return NULL; + for (size_t i = keys->v.arr.len; i-- > 0 && !scr_exc_pending();) { + ScrDyn *key = keys->v.arr.items[i]; + ScrDyn *value = scr_dyn_reflect_get(source, key, source); + if (!value) break; + scr_dyn_key_set(target, key->v.str, value); + scr_dyn_release(value); + } + scr_dyn_release(keys); + return scr_exc_pending() ? NULL : scr_dyn_retain(target); +} + /* ── util.callbackify ────────────────────────────────────────────────── * The wrapper pops the callback, binds it to the call's receiver and * calls original; the promise's settlement schedules the callback on the @@ -533,163 +593,6 @@ ScrDyn *scr_util_callbackify(ScrDyn *original) { return callbackified; } -/* ── util.format over dyn arguments ──────────────────────────────────── - * formatWithOptionsInternal for debuglog's lines: the %-substitutions - * (%s %d %i %f %j %o %O %c %%) with Node's argument cursor, then the - * remaining arguments space-joined, strings verbatim and everything else - * inspected. With terminal colors, primitives take inspect's default - * styles; colored object renderings have no lowering. */ -static bool uc_primitive(const ScrDyn *value) { - return !uc_object(value); -} - -static ScrStr *uc_inspect(ScrDyn *value, double depth, bool colors) { - if (!colors) return scr_insp_dyn(value, 0, depth); - if (!uc_primitive(value)) { - uc_fence("util.debuglog output of objects on a color terminal has no lowering"); - return NULL; - } - const char *open = "\x1b[33m", *close = "\x1b[39m"; - if (value->kind == SCR_DYN_UNDEF) open = "\x1b[90m"; - else if (value->kind == SCR_DYN_NULL) open = "\x1b[1m", close = "\x1b[22m"; - else if (value->kind == SCR_DYN_STR || value->kind == SCR_DYN_SYMBOL) open = "\x1b[32m"; - ScrStr *text = scr_insp_dyn(value, 0, depth); - ScrJsonBuf b; - scr_jb_init(&b); - scr_jb_puts(&b, open); - scr_jb_put_str(&b, text); - scr_jb_puts(&b, close); - scr_str_release(text); - return scr_jb_finish(&b); -} - -/* hasBuiltInToString's negative answer for checked objects: an own or - * inherited toString the program defined. Symbol-keyed members (where - * Symbol.toPrimitive would also count) are reported as unknown. */ -static int uc_custom_to_string(const ScrDyn *value) { - for (const ScrDyn *current = value; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { - if (current->symbol_properties) return -1; - const ScrDyn *member = scr_dyn_obj_get(current, "toString", 8); - if (member) return member->kind == SCR_DYN_FUNC; - } - return 0; -} - -static ScrStr *uc_format_s(ScrDyn *value) { - if (value->kind == SCR_DYN_STR) return scr_str_retain(value->v.str); - if (value->kind == SCR_DYN_NUM) return scr_insp_f64(value->v.num); - if (value->kind == SCR_DYN_FUNC) { - uc_fence("util.debuglog %s of a function prints its source text, which a compiled binary does not keep"); - return NULL; - } - if (value->kind == SCR_DYN_OBJ && scr_dyn_obj_get(value, "%error", 6) == NULL) { - int custom = uc_custom_to_string(value); - if (custom < 0) { - uc_fence("util.debuglog %s of objects with symbol-keyed members has no lowering"); - return NULL; - } - if (custom) return scr_dyn_string_coerce_js(value); - } - return scr_insp_dyn_s(value, 0); -} - -static ScrStr *uc_format_number(ScrDyn *value, int spec) { - if (value->kind == SCR_DYN_BIGINT && spec != 'f') return scr_bigint_inspect(value->v.bigint); - if (value->kind == SCR_DYN_SYMBOL) return scr_str_new("NaN", 3); - double number; - if (spec == 'd') { - if (value->kind == SCR_DYN_BIGINT) return scr_bigint_inspect(value->v.bigint); - if (!scr_dyn_number_coerce_js(value, &number)) return NULL; - } else { - ScrStr *text = scr_dyn_string_coerce_js(value); - if (!text) return NULL; - number = spec == 'i' ? scr_parse_int(text, 0) : scr_parse_float(text); - scr_str_release(text); - } - return scr_insp_f64(number); -} - -static ScrStr *uc_format(ScrDyn *const *args, size_t argc, bool colors) { - if (argc == 0) return scr_str_new("", 0); - ScrJsonBuf out; - scr_jb_init(&out); - size_t a = 0; - const char *join = ""; - if (args[0]->kind == SCR_DYN_STR) { - const ScrStr *first = args[0]->v.str; - if (argc == 1) { - scr_str_release(scr_jb_finish(&out)); - return scr_str_retain((ScrStr *)first); - } - const char *text = first->data; - size_t last = 0; - for (size_t i = 0; first->len > 0 && i < first->len - 1; i++) { - if (text[i] != '%') continue; - int next = (unsigned char)text[++i]; - if (a + 1 != argc) { - ScrStr *piece = NULL; - switch (next) { - case 's': piece = uc_format_s(args[++a]); break; - case 'j': piece = scr_util_format_j(args[++a]); break; - case 'd': - case 'i': - case 'f': piece = uc_format_number(args[++a], next); break; - case 'O': piece = uc_inspect(args[++a], 2, colors); break; - case 'o': - if (!uc_primitive(args[a + 1])) { - uc_fence("util.debuglog %o of objects renders hidden properties, which have no lowering; use %O"); - break; - } - piece = uc_inspect(args[++a], 4, colors); - break; - case 'c': - a += 1; - piece = scr_str_new("", 0); - break; - case '%': - scr_jb_write(&out, text + last, i - last); - last = i + 1; - continue; - default: - continue; - } - if (!piece) { - scr_str_release(scr_jb_finish(&out)); - return NULL; - } - if (last != i - 1) scr_jb_write(&out, text + last, i - 1 - last); - scr_jb_put_str(&out, piece); - scr_str_release(piece); - last = i + 1; - } else if (next == '%') { - scr_jb_write(&out, text + last, i - last); - last = i + 1; - } - } - if (last != 0) { - a++; - join = " "; - if (last < first->len) scr_jb_write(&out, text + last, first->len - last); - } - } - for (; a < argc; a++) { - scr_jb_puts(&out, join); - if (args[a]->kind == SCR_DYN_STR) { - scr_jb_put_str(&out, args[a]->v.str); - } else { - ScrStr *piece = uc_inspect(args[a], 2, colors); - if (!piece) { - scr_str_release(scr_jb_finish(&out)); - return NULL; - } - scr_jb_put_str(&out, piece); - scr_str_release(piece); - } - join = " "; - } - return scr_jb_finish(&out); -} - /* ── util.debuglog ───────────────────────────────────────────────────── * The logger defers its NODE_DEBUG test to its first call or `enabled` * read. The first call also selects the section's shared implementation @@ -755,7 +658,10 @@ static bool uc_debug_test(const ScrStr *section) { static ScrDyn *uc_debug_write(ScrClosure *closure, ScrDyn *const *args, size_t argc) { bool colors = scr_getenv("FORCE_COLOR") ? scr_util_terminal_colors() : scr_process_is_tty(2) && scr_util_terminal_colors(); - ScrStr *message = uc_format(args, argc, colors); + ScrInspFormatOptions options; + scr_insp_format_defaults(&options, "util.debuglog"); + options.colors = colors; + ScrStr *message = scr_insp_format(args, argc, &options); if (!message) return NULL; ScrDyn *set = uc_cap(closure, 0); char pid[64]; @@ -912,3 +818,158 @@ ScrDyn *scr_util_debuglog(ScrDyn *section, ScrDyn *callback) { } return logger; } + +/* ── util.diff(actual, expected) ─────────────────────────────────────── + * Node's diff over strings (one entry per UTF-16 code unit) or arrays of + * strings: identical values answer an empty array before validation; + * otherwise each value must be a string or an array of strings + * (ERR_INVALID_ARG_TYPE naming `actual`, `actual[i]`, ...), and the answer + * is myersDiff's operation list reversed: [0, item] for kept items, + * [1, item] for items of actual and [-1, item] for items of expected. */ +typedef struct { + ScrStr **items; + size_t len; +} UcDiffSeq; + +static void uc_diff_seq_free(UcDiffSeq *seq) { + for (size_t i = 0; i < seq->len; i++) scr_str_release(seq->items[i]); + free(seq->items); + seq->items = NULL; + seq->len = 0; +} + +static bool uc_diff_input(const ScrDyn *value, const char *name, UcDiffSeq *out) { + out->items = NULL; + out->len = 0; + if (value->kind == SCR_DYN_STR) { + ScrStr *text = value->v.str; + size_t len = (size_t)scr_str_utf16_len(text); + out->items = malloc((len ? len : 1) * sizeof *out->items); + if (!out->items) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; i < len; i++) out->items[out->len++] = scr_str_char_at(text, (double)i); + return true; + } + ScrDyn *array = value->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(value) + ? scr_dyn_typed_ref_materialize(value) + : value->kind == SCR_DYN_ARR ? scr_dyn_retain((ScrDyn *)value) : NULL; + if (!array) { + scr_dyn_arg_type_fail(name, "of type string", value); + return false; + } + size_t len = array->v.arr.len; + out->items = malloc((len ? len : 1) * sizeof *out->items); + if (!out->items) scr_trap("scriptc: out of memory\n"); + for (size_t i = 0; i < len; i++) { + const ScrDyn *item = array->v.arr.presence && !array->v.arr.presence[i] ? scr_dyn_undefined() + : array->v.arr.items[i]; + if (item->kind != SCR_DYN_STR) { + char element[64]; + snprintf(element, sizeof element, "%s[%zu]", name, i); + scr_dyn_arg_type_fail(element, "of type string", item); + scr_dyn_release(array); + uc_diff_seq_free(out); + return false; + } + out->items[out->len++] = scr_str_retain(item->v.str); + } + scr_dyn_release(array); + return true; +} + +static void uc_diff_push(ScrDyn *result, int op, ScrStr *item) { + ScrDyn *entry = scr_dyn_new_arr(); + scr_dyn_arr_push(entry, scr_dyn_new_num((double)op)); + scr_dyn_arr_push(entry, scr_dyn_new_str(item)); + scr_dyn_arr_push(result, entry); +} + +ScrDyn *scr_util_diff(ScrDyn *actual, ScrDyn *expected) { + if (scr_dyn_strict_eq(actual, expected)) return scr_dyn_new_arr(); + UcDiffSeq a, e; + if (!uc_diff_input(actual, "actual", &a)) return NULL; + if (!uc_diff_input(expected, "expected", &e)) { + uc_diff_seq_free(&a); + return NULL; + } + size_t max = a.len + e.len; + if (max > 2147483647u) { + uc_diff_seq_free(&a); + uc_diff_seq_free(&e); + char message[160]; + int n = snprintf(message, sizeof message, + "The value of \"myersDiff input size\" is out of range. It must be < 2^31. Received %zu", max); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)n, "ERR_OUT_OF_RANGE"); + return NULL; + } + /* Two distinct empty sequences: Node's myersDiff reads past its + * one-element state, finds no end and answers undefined, which the + * ArrayPrototypeReverse of util.diff rejects. The trace below would be read + * out of bounds. */ + if (max == 0) { + uc_diff_seq_free(&a); + uc_diff_seq_free(&e); + static const char message[] = "Cannot convert undefined or null to object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + size_t width = 2 * max + 1; + int32_t *v = calloc(width, sizeof *v); + int32_t **trace = malloc((max + 1) * sizeof *trace); + if (!v || !trace) scr_trap("scriptc: out of memory\n"); + size_t levels = 0; + bool done = false; + for (size_t level = 0; level <= max && !done; level++) { + trace[levels] = malloc(width * sizeof **trace); + if (!trace[levels]) scr_trap("scriptc: out of memory\n"); + memcpy(trace[levels], v, width * sizeof *v); + levels++; + long d = (long)level; + for (long k = -d; k <= d; k += 2) { + size_t offset = (size_t)(k + (long)max); + int32_t previous = offset > 0 ? v[offset - 1] : 0; + int32_t next = offset + 1 < width ? v[offset + 1] : 0; + long x = k == -d || (k != d && previous < next) ? next : (long)previous + 1; + long y = x - k; + while ((size_t)x < a.len && y >= 0 && (size_t)y < e.len && scr_str_eq(a.items[x], e.items[y])) { + x++; + y++; + } + v[offset] = (int32_t)x; + if ((size_t)x >= a.len && y >= 0 && (size_t)y >= e.len) { + done = true; + break; + } + } + } + /* backtrack: the operations from the end, which the answer reverses. */ + ScrDyn *reversed = scr_dyn_new_arr(); + long x = (long)a.len, y = (long)e.len; + for (long level = (long)levels - 1; level >= 0; level--) { + const int32_t *row = trace[level]; + long k = x - y; + size_t offset = (size_t)(k + (long)max); + long prev_k; + if (k == -level || (k != level && row[offset - 1] < row[offset + 1])) prev_k = k + 1; + else prev_k = k - 1; + long prev_x = row[(size_t)(prev_k + (long)max)]; + long prev_y = prev_x - prev_k; + while (x > prev_x && y > prev_y) { + uc_diff_push(reversed, 0, a.items[x - 1]); + x--; + y--; + } + if (level > 0) { + if (x > prev_x) uc_diff_push(reversed, 1, a.items[--x]); + else uc_diff_push(reversed, -1, e.items[--y]); + } + } + for (size_t i = 0; i < levels; i++) free(trace[i]); + free(trace); + free(v); + ScrDyn *result = scr_dyn_new_arr(); + for (size_t i = reversed->v.arr.len; i-- > 0;) scr_dyn_arr_push(result, scr_dyn_retain(reversed->v.arr.items[i])); + scr_dyn_release(reversed); + uc_diff_seq_free(&a); + uc_diff_seq_free(&e); + return result; +} diff --git a/packages/runtime/src/scr_util_timing.c b/packages/runtime/src/scr_util_timing.c new file mode 100644 index 000000000..13651f5ff --- /dev/null +++ b/packages/runtime/src/scr_util_timing.c @@ -0,0 +1,1062 @@ +/* node:util's debounce and throttle over the checked-dynamic tree, step + * for step with Node's lib/internal/util/debounce.js and throttle.js. + * Each returns a native dyn function (an ordinary function with its own + * prototype, fn's name and length) whose own cancel/flush/ref/unref + * members (throttle: cancel/hasImmediateCapacity/ref/unref) and + * non-enumerable pending/pendingCount (throttle: and activeCount) getters + * share one refcounted state. Calls answer dyn promises. + * + * The state is invisible to the cycle collector, so nothing it holds + * leads back to it: fn sits in a traced box on the returned function's + * closure (fn may refer to that function), the state reaches the function through + * a weak pointer that the function's closure clears when it is freed; an + * armed timer and a throttled call's settle reaction hold the function + * and the state until they run or are cleared; and the options.signal + * 'abort' listener holds a weak slot that the state clears when it is + * freed. The listener stays registered, as in Node. The clock is the + * monotonic loop clock in whole milliseconds, like libuv's uv_now. All + * arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +/* Node's TIMEOUT_MAX (2 ** 31 - 1) and Number.MAX_SAFE_INTEGER. */ +#define UT_TIMEOUT_MAX 2147483647.0 +#define UT_SAFE_MAX 9007199254740991.0 + +typedef struct UtState UtState; + +/* The abort listener's view of a state: NULL once the state is freed. */ +typedef struct { + size_t rc; + UtState *state; +} UtWeak; + +/* The returned function's closure holds its state through this token; + * freeing the closure frees the token, which clears the state's weak + * pointer to the function. */ +typedef struct { + size_t rc; + UtState *state; +} UtOwner; + +/* One pending call: its promise and, for throttle, its arguments. */ +typedef struct { + ScrPromise *promise; + ScrDyn **args; + size_t argc; +} UtCall; + +struct UtState { + size_t rc; + bool throttle; + ScrClosure *self; /* the returned function's closure (weak), or NULL */ + ScrDynThunk thunk; + ScrDyn *signal; /* NULL without options.signal */ + UtWeak *weak; /* the abort listener's slot, or NULL */ + ScrDyn *abort_error; /* NULL until the signal aborts */ + double timeout; /* the armed timer's handle; 0 for none */ + bool refed; + /* pendingCalls: the live calls are [head, len). */ + UtCall *calls; + size_t head, len, cap; + /* debounce */ + double wait; + bool leading, reject_on_cancel; + ScrDyn **args; /* the latest call's arguments */ + size_t argc; + /* throttle */ + double limit, interval, concurrency, max_pending, active; + bool drop, strict; + double *ticks; /* strictTicks: the live ticks are [tick_head, tick_len) */ + size_t tick_head, tick_len, tick_cap; + double window_count, window_start; + bool window_open; +}; + +static const char UT_SIG[] = "native:util.debounce"; + +static void *ut_oom(void) { + scr_trap("scriptc: out of memory\n"); +} + +static ScrDyn **ut_args_copy(ScrDyn *const *args, size_t argc) { + if (argc == 0) return NULL; + ScrDyn **copy = malloc(argc * sizeof *copy); + if (!copy) return ut_oom(); + for (size_t i = 0; i < argc; i++) copy[i] = scr_dyn_retain(args[i]); + return copy; +} + +static void ut_args_free(ScrDyn **args, size_t argc) { + for (size_t i = 0; i < argc; i++) scr_dyn_release(args[i]); + free(args); +} + +static void *ut_weak_retain(void *p) { + ((UtWeak *)p)->rc++; + return p; +} + +static void ut_weak_release(void *p) { + UtWeak *weak = p; + if (weak && --weak->rc == 0) free(weak); +} + +static void *ut_state_retain(void *p) { + ((UtState *)p)->rc++; + return p; +} + +static void ut_state_release(void *p) { + UtState *s = p; + if (!s || --s->rc) return; + if (s->weak) { + s->weak->state = NULL; + ut_weak_release(s->weak); + } + scr_dyn_release(s->signal); + scr_dyn_release(s->abort_error); + ut_args_free(s->args, s->argc); + for (size_t i = s->head; i < s->len; i++) { + scr_promise_release(s->calls[i].promise); + ut_args_free(s->calls[i].args, s->calls[i].argc); + } + free(s->calls); + free(s->ticks); + free(s); +} + +static void *ut_owner_retain(void *p) { + ((UtOwner *)p)->rc++; + return p; +} + +static void ut_owner_release(void *p) { + UtOwner *owner = p; + if (--owner->rc) return; + owner->state->self = NULL; + ut_state_release(owner->state); + free(owner); +} + +static ScrBox *ut_state_box(UtState *s) { + ScrBox *box = scr_box_new_obj(ut_state_retain, ut_state_release, NULL); + scr_box_set_ref(box, ut_state_retain(s)); + return box; +} + +static ScrBox *ut_dyn_box(ScrDyn *value /* moves in */) { + ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(box, value); + return box; +} + +/* The state behind a member, getter, timer or listener closure (+1). */ +static UtState *ut_cap_state(ScrClosure *closure) { + return scr_box_get_ref(closure->caps[0]); +} + +/* The state behind the returned function's own closure (+1). */ +static UtState *ut_owner_state(ScrClosure *closure) { + UtOwner *owner = scr_box_get_ref(closure->caps[0]); + UtState *s = ut_state_retain(owner->state); + ut_owner_release(owner); + return s; +} + +static ScrDyn *ut_box_fn(UtState *s, ScrClosure *closure) { + return scr_dyn_new_func(scr_closure_retain(closure), s->thunk, 0, UT_SIG, s->throttle ? "throttled" : "debounced"); +} + +/* The returned function (+1), or NULL once nothing else holds it. */ +static ScrDyn *ut_self(UtState *s) { + return s->self ? ut_box_fn(s, s->self) : NULL; +} + +static void ut_fence(const char *message) { + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); +} + +static double ut_now(void) { + return floor(scr_now_ms()); +} + +/* ── promises and errors ───────────────────────────────────────────── */ + +static void ut_reject(ScrPromise *p, const ScrDyn *error) { + scr_dyn_throw(scr_dyn_retain((ScrDyn *)error)); + scr_promise_reject_pending(p); +} + +static ScrDyn *ut_promise_box(ScrPromise *p /* moves in */) { + ScrDyn *box = scr_dyn_new_promise(p); + scr_promise_release(p); + return box; +} + +/* PromiseReject(error) (+1). */ +static ScrDyn *ut_rejected(const ScrDyn *error) { + ScrPromise *p = scr_promise_new(); + ut_reject(p, error); + return ut_promise_box(p); +} + +/* new AbortError(message, { cause }) (+1); a NULL cause defines none. */ +static ScrDyn *ut_abort_error(const char *message, const ScrDyn *cause) { + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + if (cause) { + /* The constructor's { cause } option: a non-enumerable own property. */ + scr_error_define_cause(e, (ScrDyn *)cause); + e->cause_enumerable = false; + } + ScrDyn *error = scr_dyn_from_error(e); + scr_error_release(e); + return error; +} + +/* cancel(reason)'s error: no cause for an undefined reason (+1). */ +static ScrDyn *ut_cancel_error(ScrDyn *const *args, size_t argc) { + const ScrDyn *reason = argc ? args[0] : scr_dyn_undefined(); + return ut_abort_error("The operation was aborted", reason->kind == SCR_DYN_UNDEF ? NULL : reason); +} + +/* fn.apply(self, args): the result (+1), or NULL with fn's throw pending. + * fn lives on the returned function's closure. */ +static ScrDyn *ut_apply(const ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(scr_closure_identity(self->v.fn.clo)->caps[1]); + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, "fn"); + scr_dyn_this_pop(); + scr_dyn_release(fn); + return result; +} + +/* ── the signal ────────────────────────────────────────────────────── */ + +static bool ut_signal_aborted(const UtState *s) { + if (!s->signal || !scr_et_signal_core_fn) return false; + bool aborted = false; + (void)scr_et_signal_core_fn(s->signal, &aborted); + return aborted; +} + +static void ut_cancel_with_error(UtState *s, const ScrDyn *error); + +/* abort(): the first abort builds abortError from signal.reason. */ +static void ut_abort(UtState *s) { + if (!s->abort_error) { + ScrStr *key = scr_str_new("reason", 6); + ScrDyn *reason = scr_dyn_handle_key_get(s->signal, key); + scr_str_release(key); + if (!reason) return; + s->abort_error = ut_abort_error("The operation was aborted", reason->kind == SCR_DYN_UNDEF ? NULL : reason); + scr_dyn_release(reason); + } + ut_cancel_with_error(s, s->abort_error); +} + +static ScrDyn *ut_abort_listener(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtWeak *weak = scr_box_get_ref(closure->caps[0]); + UtState *s = weak->state ? ut_state_retain(weak->state) : NULL; + ut_weak_release(weak); + if (s) { + ut_abort(s); + ut_state_release(s); + } + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +/* addAbortListener(signal, abort). */ +static bool ut_listen(UtState *s) { + UtWeak *weak = malloc(sizeof *weak); + if (!weak) ut_oom(); + weak->rc = 1; + weak->state = s; + s->weak = weak; + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(ut_weak_retain, ut_weak_release, NULL); + scr_box_set_ref(closure->caps[0], ut_weak_retain(weak)); + ScrDyn *listener = scr_dyn_new_func(closure, ut_abort_listener, 0, UT_SIG, "abort"); + ScrDyn *disposable = scr_events_add_abort_listener(s->signal, listener); + scr_dyn_release(listener); + scr_dyn_release(disposable); + return !scr_exc_pending(); +} + +/* ── pending calls ─────────────────────────────────────────────────── */ + +static size_t ut_pending_count(const UtState *s) { + return s->len - s->head; +} + +static void ut_push(UtState *s, ScrPromise *promise /* moves in */, ScrDyn **args /* moves in */, size_t argc) { + /* Calls already taken free their slots, so a queue that never empties + * keeps its storage within twice its live calls. */ + if (s->len == s->cap && s->head > 0) { + s->len -= s->head; + memmove(s->calls, s->calls + s->head, s->len * sizeof *s->calls); + s->head = 0; + } + if (s->len == s->cap) { + size_t cap = s->cap ? s->cap * 2 : 4; + UtCall *grown = realloc(s->calls, cap * sizeof *grown); + if (!grown) ut_oom(); + s->calls = grown; + s->cap = cap; + } + s->calls[s->len++] = (UtCall){promise, args, argc}; +} + +/* Takes the pending calls, leaving none: *count live calls at the result + * (free it, the promises and arguments when done). */ +static UtCall *ut_take_calls(UtState *s, size_t *count) { + UtCall *calls = s->calls; + size_t head = s->head; + *count = s->len - head; + if (head) memmove(calls, calls + head, *count * sizeof *calls); + s->calls = NULL; + s->head = s->len = s->cap = 0; + return calls; +} + +static void ut_calls_free(UtCall *calls, size_t count) { + for (size_t i = 0; i < count; i++) { + scr_promise_release(calls[i].promise); + ut_args_free(calls[i].args, calls[i].argc); + } + free(calls); +} + +/* rejectPending(error). */ +static void ut_reject_pending(UtState *s, const ScrDyn *error) { + size_t count; + UtCall *calls = ut_take_calls(s, &count); + for (size_t i = 0; i < count; i++) ut_reject(calls[i].promise, error); + ut_calls_free(calls, count); +} + +static void ut_clear_timer(UtState *s) { + double timeout = s->timeout; + s->timeout = 0; + scr_clear_interval(timeout); +} + +static void ut_arm(UtState *s, const ScrDyn *self, double delay, void (*fire)(ScrClosure *)) { + ScrClosure *timer = scr_closure_new((void *)fire, 2); + timer->caps[0] = ut_state_box(s); + timer->caps[1] = ut_dyn_box(scr_dyn_retain((ScrDyn *)self)); + s->timeout = scr_set_timeout_handle(timer, delay); + if (!s->refed) scr_timer_unref(s->timeout); +} + +/* The timer's state (+1) and function (+1). */ +static UtState *ut_timer_state(ScrClosure *timer, ScrDyn **self) { + *self = scr_box_get_ref(timer->caps[1]); + return ut_cap_state(timer); +} + +/* ── throttle's limiter ────────────────────────────────────────────── */ + +static void th_reset_limiter(UtState *s) { + s->tick_head = s->tick_len = 0; + s->window_count = 0; + s->window_open = false; +} + +static void th_prune_ticks(UtState *s, double now) { + while (s->tick_head < s->tick_len && now - s->ticks[s->tick_head] >= s->interval) s->tick_head++; + if (s->tick_head == s->tick_len) s->tick_head = s->tick_len = 0; +} + +static bool th_has_capacity(UtState *s, double now) { + if (s->strict) { + th_prune_ticks(s, now); + return (double)(s->tick_len - s->tick_head) < s->limit; + } + if (!s->window_open || now - s->window_start >= s->interval) { + s->window_count = 0; + s->window_start = now; + s->window_open = true; + } + return s->window_count < s->limit; +} + +static void th_reserve(UtState *s, double now) { + if (!s->strict) { + s->window_count++; + return; + } + /* Ticks that left the window free their slots: at most limit ticks are + * live, so the storage stays within twice that. */ + if (s->tick_len == s->tick_cap && s->tick_head > 0) { + s->tick_len -= s->tick_head; + memmove(s->ticks, s->ticks + s->tick_head, s->tick_len * sizeof *s->ticks); + s->tick_head = 0; + } + if (s->tick_len == s->tick_cap) { + size_t cap = s->tick_cap ? s->tick_cap * 2 : 8; + double *grown = realloc(s->ticks, cap * sizeof *grown); + if (!grown) ut_oom(); + s->ticks = grown; + s->tick_cap = cap; + } + s->ticks[s->tick_len++] = now; +} + +static void ut_cancel_with_error(UtState *s, const ScrDyn *error) { + if (!s->throttle) { + /* debounce's cancelWithError */ + if (s->timeout == 0) return; + ut_clear_timer(s); + ut_args_free(s->args, s->argc); + s->args = NULL; + s->argc = 0; + ut_reject_pending(s, error); + return; + } + if (s->timeout != 0) ut_clear_timer(s); + th_reset_limiter(s); + ut_reject_pending(s, error); +} + +/* ── debounce ──────────────────────────────────────────────────────── */ + +/* invoke(callArgs, calls): every call settles with fn's one result. */ +static void db_invoke(UtState *s, const ScrDyn *self, ScrDyn *const *args, size_t argc, UtCall *calls, size_t count) { + if (ut_signal_aborted(s)) { + ut_abort(s); + if (!s->abort_error) return; + for (size_t i = 0; i < count; i++) ut_reject(calls[i].promise, s->abort_error); + return; + } + ScrDyn *result = ut_apply(self, args, argc); + if (!result) { + scr_promise_reject_pending(calls[0].promise); + ScrDyn *error = scr_promise_reason_dyn(calls[0].promise); + for (size_t i = 1; i < count; i++) ut_reject(calls[i].promise, error); + scr_dyn_release(error); + return; + } + for (size_t i = 0; i < count; i++) scr_promise_resolve_dyn(calls[i].promise, scr_dyn_retain(result)); + scr_dyn_release(result); +} + +static void db_invoke_pending(UtState *s, const ScrDyn *self) { + ScrDyn **args = s->args; + size_t argc = s->argc; + s->args = NULL; + s->argc = 0; + size_t count; + UtCall *calls = ut_take_calls(s, &count); + if (count) db_invoke(s, self, args, argc, calls, count); + ut_calls_free(calls, count); + ut_args_free(args, argc); +} + +static void db_on_timeout(ScrClosure *timer) { + ScrDyn *self; + UtState *s = ut_timer_state(timer, &self); + s->timeout = 0; + db_invoke_pending(s, self); + scr_dyn_release(self); + ut_state_release(s); +} + +static ScrDyn *db_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + UtState *s = ut_owner_state(closure); + ScrDyn *self = ut_box_fn(s, closure); + ScrDyn *out = NULL; + if (ut_signal_aborted(s)) ut_abort(s); + if (scr_exc_pending()) goto done; + if (s->abort_error) { + out = ut_rejected(s->abort_error); + goto done; + } + bool invoke_now = s->leading && s->timeout == 0; + if (s->timeout != 0) { + if (ut_pending_count(s) != 0) { + scr_promise_mark_handled(s->calls[s->len - 1].promise); + if (s->reject_on_cancel) { + ScrDyn *error = ut_abort_error("The debounced call was superseded", NULL); + ut_reject_pending(s, error); + scr_dyn_release(error); + } + } + scr_timer_refresh(s->timeout); + } else { + ut_arm(s, self, s->wait, db_on_timeout); + } + if (ut_signal_aborted(s)) { + ut_abort(s); + if (s->abort_error) out = ut_rejected(s->abort_error); + goto done; + } + ScrPromise *promise = scr_promise_new(); + if (invoke_now) { + UtCall call = {promise, NULL, 0}; + db_invoke(s, self, args, argc, &call, 1); + } else { + ut_push(s, scr_promise_retain(promise), NULL, 0); + ut_args_free(s->args, s->argc); + s->args = ut_args_copy(args, argc); + s->argc = argc; + } + out = ut_promise_box(promise); +done: + scr_dyn_release(self); + ut_state_release(s); + return out; +} + +static ScrDyn *db_flush(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + if (ut_pending_count(s) != 0) { + /* The armed timer holds the function until it is cleared. */ + ScrDyn *self = ut_self(s); + ut_clear_timer(s); + db_invoke_pending(s, self); + scr_dyn_release(self); + } + ut_state_release(s); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +/* ── throttle ──────────────────────────────────────────────────────── */ + +static void th_drain(UtState *s, const ScrDyn *self); + +static ScrDyn *th_on_settled(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrDyn *self; + UtState *s = ut_timer_state(closure, &self); + s->active--; + th_drain(s, self); + scr_dyn_release(self); + ut_state_release(s); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +/* invoke(call): the call adopts PromiseResolve(fn's result), and the + * active count drops when that promise settles. Moves the call in. */ +static void th_invoke(UtState *s, const ScrDyn *self, UtCall call) { + s->active++; + ScrDyn *result = ut_apply(self, call.args, call.argc); + ScrPromise *settled; + if (!result) { + settled = scr_promise_new(); + scr_promise_reject_pending(settled); + } else if (scr_dyn_promise_of(result)) { + settled = scr_promise_retain(scr_dyn_promise_of(result)); + scr_dyn_release(result); + } else { + settled = scr_promise_new(); + scr_promise_resolve_dyn(settled, result); + } + scr_promise_resolve_dyn(call.promise, scr_dyn_new_promise(settled)); + ScrClosure *reaction = scr_closure_new(NULL, 2); + reaction->caps[0] = ut_state_box(s); + reaction->caps[1] = ut_dyn_box(scr_dyn_retain((ScrDyn *)self)); + ScrDyn *on_settled = scr_dyn_new_func(reaction, th_on_settled, 0, UT_SIG, "onSettled"); + scr_dyn_release(scr_dyn_promise_then(settled, on_settled, on_settled, NULL)); + scr_dyn_release(on_settled); + scr_promise_release(settled); + scr_promise_release(call.promise); + ut_args_free(call.args, call.argc); +} + +static void th_on_timeout(ScrClosure *timer); + +static void th_schedule(UtState *s, const ScrDyn *self) { + if (s->timeout != 0 || ut_pending_count(s) == 0 || s->active >= s->concurrency) return; + double now = ut_now(); + if (th_has_capacity(s, now)) { + th_drain(s, self); + return; + } + double delay = s->strict ? s->ticks[s->tick_head] + s->interval - now : s->window_start + s->interval - now; + ut_arm(s, self, delay, th_on_timeout); +} + +static void th_drain(UtState *s, const ScrDyn *self) { + if (ut_signal_aborted(s)) { + ut_abort(s); + return; + } + while (ut_pending_count(s) != 0 && s->active < s->concurrency) { + double now = ut_now(); + if (!th_has_capacity(s, now)) break; + UtCall call = s->calls[s->head++]; + if (s->head == s->len) s->head = s->len = 0; + th_reserve(s, now); + th_invoke(s, self, call); + } + th_schedule(s, self); +} + +static void th_on_timeout(ScrClosure *timer) { + ScrDyn *self; + UtState *s = ut_timer_state(timer, &self); + s->timeout = 0; + th_drain(s, self); + scr_dyn_release(self); + ut_state_release(s); +} + +/* drop(): a handled rejection with ERR_THROTTLED (+1). */ +static ScrDyn *th_drop(void) { + static const char message[] = "The call was dropped by the throttled function"; + ScrStr *text = scr_str_new(message, sizeof message - 1); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_error_set_code(e, "ERR_THROTTLED"); + ScrDyn *error = scr_dyn_from_error(e); + scr_error_release(e); + ScrPromise *p = scr_promise_new(); + ut_reject(p, error); + scr_dyn_release(error); + scr_promise_mark_handled(p); + return ut_promise_box(p); +} + +static ScrDyn *th_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + UtState *s = ut_owner_state(closure); + ScrDyn *self = ut_box_fn(s, closure); + ScrDyn *out = NULL; + if (ut_signal_aborted(s)) ut_abort(s); + if (scr_exc_pending()) goto done; + if (s->abort_error) { + out = ut_rejected(s->abort_error); + goto done; + } + double now = ut_now(); + bool can_invoke = ut_pending_count(s) == 0 && s->active < s->concurrency && th_has_capacity(s, now); + if (!can_invoke && (s->drop || (double)ut_pending_count(s) >= s->max_pending)) { + out = th_drop(); + goto done; + } + ScrPromise *promise = scr_promise_new(); + out = scr_dyn_new_promise(promise); + UtCall call = {promise, ut_args_copy(args, argc), argc}; + if (can_invoke) { + th_reserve(s, now); + th_invoke(s, self, call); + } else { + ut_push(s, call.promise, call.args, call.argc); + th_schedule(s, self); + } +done: + scr_dyn_release(self); + ut_state_release(s); + return out; +} + +static ScrDyn *th_has_immediate_capacity(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + bool capacity = false; + if (!ut_signal_aborted(s) && ut_pending_count(s) == 0 && s->active < s->concurrency) { + double now = ut_now(); + if (s->strict) { + size_t index = s->tick_head; + while (index < s->tick_len && now - s->ticks[index] >= s->interval) index++; + capacity = (double)(s->tick_len - index) < s->limit; + } else { + capacity = !s->window_open || now - s->window_start >= s->interval || s->window_count < s->limit; + } + } + ut_state_release(s); + return scr_dyn_new_bool(capacity); +} + +/* ── the shared members ────────────────────────────────────────────── */ + +static ScrDyn *ut_cancel(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + UtState *s = ut_cap_state(closure); + if (s->throttle || s->timeout != 0) { + ScrDyn *error = ut_cancel_error(args, argc); + ut_cancel_with_error(s, error); + scr_dyn_release(error); + } + ut_state_release(s); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *ut_set_ref(ScrClosure *closure, bool refed) { + UtState *s = ut_cap_state(closure); + s->refed = refed; + if (s->timeout != 0) { + if (refed) scr_timer_ref(s->timeout); + else scr_timer_unref(s->timeout); + } + ScrDyn *self = ut_self(s); + if (!self) + ut_fence(s->throttle ? "util.throttle ref() and unref() after the throttled function is unreachable have no lowering" + : "util.debounce ref() and unref() after the debounced function is unreachable have no lowering"); + ut_state_release(s); + return self; +} + +static ScrDyn *ut_ref(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + return ut_set_ref(closure, true); +} + +static ScrDyn *ut_unref(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + return ut_set_ref(closure, false); +} + +static ScrDyn *ut_pending(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + ScrDyn *out = ut_pending_count(s) == 0 ? scr_dyn_new_null() : scr_dyn_new_promise(s->calls[s->len - 1].promise); + ut_state_release(s); + return out; +} + +static ScrDyn *ut_pending_count_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + ScrDyn *out = scr_dyn_new_num((double)ut_pending_count(s)); + ut_state_release(s); + return out; +} + +static ScrDyn *th_active_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + UtState *s = ut_cap_state(closure); + ScrDyn *out = scr_dyn_new_num(s->active); + ut_state_release(s); + return out; +} + +/* ── construction ──────────────────────────────────────────────────── */ + +/* Object.defineProperty(target, key, descriptor); the descriptor moves in. */ +static bool ut_define(ScrDyn *target, const char *key, ScrDyn *descriptor) { + ScrStr *name = scr_str_new(key, strlen(key)); + ScrDyn *property = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *defined = scr_dyn_define_property(target, property, descriptor); + scr_dyn_release(property); + scr_dyn_release(descriptor); + scr_dyn_release(defined); + return defined != NULL; +} + +static ScrDyn *ut_member_fn(UtState *s, ScrDynThunk thunk, uint32_t arity, const char *name) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = ut_state_box(s); + return scr_dyn_new_func(closure, thunk, arity, UT_SIG, name); +} + +/* createFn(value): a writable, enumerable, configurable method. */ +static bool ut_define_method(ScrDyn *target, UtState *s, const char *key, ScrDynThunk thunk, uint32_t arity) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "value", 5, ut_member_fn(s, thunk, arity, key)); + return ut_define(target, key, descriptor); +} + +static bool ut_define_getter(ScrDyn *target, UtState *s, const char *key, ScrDynThunk thunk, const char *name) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false)); + scr_dyn_obj_set(descriptor, "get", 3, ut_member_fn(s, thunk, 0, name)); + return ut_define(target, key, descriptor); +} + +/* `length` and `name` take fn's values. */ +static bool ut_define_value(ScrDyn *target, const char *key, ScrDyn *value /* moves in */) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "value", 5, value); + return ut_define(target, key, descriptor); +} + +/* `value[key]` over an options object (+1, or NULL with an exception + * pending). */ +static ScrDyn *ut_get(const ScrDyn *value, const char *key) { + size_t len = strlen(key); + ScrDyn *result = NULL; + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { + scr_dyn_release(view); + return NULL; + } + result = ut_get(view, key); + scr_dyn_release(view); + return result; + } + if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_PROXY) { + result = scr_dyn_obj_read(value, key, len); + } else if (value->kind == SCR_DYN_HANDLE) { + ScrStr *name = scr_str_new(key, len); + result = scr_dyn_handle_key_get(value, name); + scr_str_release(name); + return result; + } else { + return scr_dyn_retain(scr_dyn_undefined()); + } + if (!result && !scr_exc_pending()) result = scr_dyn_retain(scr_dyn_undefined()); + return result; +} + +static void ut_type_fail(const char *name, const char *expected, const ScrDyn *got) { + if (strchr(name, '.')) scr_dyn_prop_type_fail(name, expected, got); + else scr_dyn_arg_type_fail(name, expected, got); +} + +/* validateFunction(fn, 'fn'). */ +static bool ut_function_arg(const ScrDyn *fn, const char *api) { + if (fn->kind == SCR_DYN_PROXY && scr_dyn_is_callable(fn)) { + char message[96]; + snprintf(message, sizeof message, "util.%s over Proxy functions has no lowering", api); + ut_fence(message); + return false; + } + if (fn->kind != SCR_DYN_FUNC) { + ut_type_fail("fn", "of type function", fn); + return false; + } + return true; +} + +/* validateInteger(value, name, min, max). */ +static bool ut_integer(const ScrDyn *value, const char *name, double min, double max, double *out) { + if (value->kind != SCR_DYN_NUM) { + ut_type_fail(name, "of type number", value); + return false; + } + double v = value->v.num; + bool integer = isfinite(v) && trunc(v) == v; + if (integer && v >= min && v <= max) { + *out = v; + return true; + } + char received[48]; + if (v == 0 && signbit(v)) memcpy(received, "-0", 3); + else scr_num_received(v, received); + char range[64]; + if (integer) snprintf(range, sizeof range, ">= %.0f && <= %.0f", min, max); + else memcpy(range, "an integer", 11); + char message[256]; + int n = snprintf(message, sizeof message, "The value of \"%s\" is out of range. It must be %s. Received %s", name, + range, received); + scr_throw_error_msg_code(SCR_ERR_RANGE, message, (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + +/* validateBoolean(value, name) over a defaulted option. */ +static bool ut_boolean(const ScrDyn *value, const char *name, bool *out) { + if (value->kind == SCR_DYN_UNDEF) { + *out = false; + return true; + } + if (value->kind != SCR_DYN_BOOL) { + ut_type_fail(name, "of type boolean", value); + return false; + } + *out = value->v.b; + return true; +} + +/* validateObject(options, 'options'), with undefined as the empty object. */ +static bool ut_options_object(const ScrDyn *options) { + if (options->kind == SCR_DYN_UNDEF) return true; + bool object = options->kind == SCR_DYN_OBJ || options->kind == SCR_DYN_HANDLE || options->kind == SCR_DYN_BYTES || + options->kind == SCR_DYN_PROMISE || (options->kind == SCR_DYN_PROXY && !scr_dyn_is_callable(options)); + if (options->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(options); + if (!view) return false; + object = view->kind == SCR_DYN_OBJ; + scr_dyn_release(view); + } + if (!object) ut_type_fail("options", "of type object", options); + return object; +} + +/* validateAbortSignal(signal, 'options.signal'), then Node's throw for an + * already-aborted signal. Objects that pass Node's duck-typed check + * without being AbortSignal instances are refused. */ +static bool ut_signal_option(const ScrDyn *signal, const char *api) { + if (signal->kind == SCR_DYN_UNDEF) return true; + bool aborted = false; + if (scr_et_signal_core_fn && scr_et_signal_core_fn(signal, &aborted)) { + if (!aborted) return true; + ScrStr *key = scr_str_new("reason", 6); + ScrDyn *reason = scr_dyn_handle_key_get(signal, key); + scr_str_release(key); + if (!reason) return false; + ScrDyn *error = ut_abort_error("The operation was aborted", reason); + scr_dyn_release(reason); + scr_dyn_throw(error); + return false; + } + bool object = signal->kind == SCR_DYN_OBJ || signal->kind == SCR_DYN_TYPED_REF || signal->kind == SCR_DYN_HANDLE || + signal->kind == SCR_DYN_PROXY; + ScrStr *key = scr_str_new("aborted", 7); + bool duck = object && signal->kind != SCR_DYN_HANDLE && scr_dyn_in(signal, key); + scr_str_release(key); + if (scr_exc_pending()) return false; + if (duck) { + char message[112]; + snprintf(message, sizeof message, "util.%s options.signal values that are not AbortSignal instances have no lowering", + api); + ut_fence(message); + return false; + } + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); + return false; +} + +static UtState *ut_state_new(bool throttle, ScrDyn *signal) { + UtState *s = calloc(1, sizeof *s); + if (!s) ut_oom(); + s->rc = 1; + s->throttle = throttle; + s->thunk = throttle ? th_call : db_call; + s->signal = signal->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_retain(signal); + s->refed = true; + return s; +} + +/* The returned function, its members and getters, fn's length and name, + * and the abort listener. Consumes the caller's state reference. fn sits + * in a traced box on the function's closure, so a cycle through it (fn + * refers to the returned function) stays collectable. */ +static ScrDyn *ut_build(UtState *s, ScrDyn *wrapped) { + ScrClosure *closure = scr_closure_new(NULL, 2); + closure->function_kind = 4; /* an ordinary function with its own prototype */ + UtOwner *owner = malloc(sizeof *owner); + if (!owner) ut_oom(); + owner->rc = 1; + owner->state = s; /* the caller's reference */ + closure->caps[0] = scr_box_new_obj(ut_owner_retain, ut_owner_release, NULL); + scr_box_set_ref(closure->caps[0], owner); + closure->caps[1] = ut_dyn_box(scr_dyn_retain(wrapped)); + s->self = closure; + ScrDyn *fn = ut_box_fn(s, closure); + scr_closure_release(closure); + bool ok = ut_define_method(fn, s, "cancel", ut_cancel, 1); + if (s->throttle) { + ok = ok && ut_define_method(fn, s, "hasImmediateCapacity", th_has_immediate_capacity, 0); + } else { + ok = ok && ut_define_method(fn, s, "flush", db_flush, 0); + } + ok = ok && ut_define_method(fn, s, "ref", ut_ref, 0) && ut_define_method(fn, s, "unref", ut_unref, 0) && + ut_define_getter(fn, s, "pending", ut_pending, "get") && + ut_define_getter(fn, s, "pendingCount", ut_pending_count_get, s->throttle ? "pendingCount" : "get"); + if (s->throttle) ok = ok && ut_define_getter(fn, s, "activeCount", th_active_count, "get"); + if (ok) { + ScrDyn *length = scr_dyn_fn_get(wrapped, "length", 6); + ok = ut_define_value(fn, "length", length ? length : scr_dyn_retain(scr_dyn_undefined())); + } + if (ok) { + ScrDyn *name = scr_dyn_fn_get(wrapped, "name", 4); + ok = ut_define_value(fn, "name", name ? name : scr_dyn_retain(scr_dyn_undefined())); + } + if (ok && s->signal) ok = ut_listen(s); + if (!ok) { + scr_dyn_release(fn); + return NULL; + } + return fn; +} + +ScrDyn *scr_util_debounce(ScrDyn *fn, ScrDyn *wait, ScrDyn *options) { + double ms; + if (!ut_function_arg(fn, "debounce") || !ut_integer(wait, "wait", 0, UT_TIMEOUT_MAX, &ms) || + !ut_options_object(options)) + return NULL; + ScrDyn *leading = ut_get(options, "leading"); + ScrDyn *reject_on_cancel = leading ? ut_get(options, "rejectOnCancel") : NULL; + ScrDyn *signal = reject_on_cancel ? ut_get(options, "signal") : NULL; + bool lead = false, reject = false; + bool ok = signal && ut_boolean(leading, "options.leading", &lead) && + ut_boolean(reject_on_cancel, "options.rejectOnCancel", &reject) && ut_signal_option(signal, "debounce"); + ScrDyn *result = NULL; + if (ok) { + UtState *s = ut_state_new(false, signal); + s->wait = ms; + s->leading = lead; + s->reject_on_cancel = reject; + result = ut_build(s, fn); + } + scr_dyn_release(signal); + scr_dyn_release(reject_on_cancel); + scr_dyn_release(leading); + return result; +} + +/* `= Infinity` options: validateInteger(value, name, min) unless Infinity. */ +static bool ut_limit_option(const ScrDyn *value, const char *name, double min, double *out) { + if (value->kind == SCR_DYN_UNDEF || (value->kind == SCR_DYN_NUM && value->v.num == INFINITY)) { + *out = INFINITY; + return true; + } + return ut_integer(value, name, min, UT_SAFE_MAX, out); +} + +ScrDyn *scr_util_throttle(ScrDyn *fn, ScrDyn *limit, ScrDyn *interval, ScrDyn *options) { + double count, ms; + if (!ut_function_arg(fn, "throttle") || !ut_integer(limit, "limit", 1, UT_SAFE_MAX, &count) || + !ut_integer(interval, "interval", 0, UT_TIMEOUT_MAX, &ms) || !ut_options_object(options)) + return NULL; + static const char *const keys[] = {"concurrency", "maxPending", "overflow", "signal", "strict"}; + ScrDyn *values[5] = {NULL}; + bool ok = true; + for (size_t i = 0; i < 5 && ok; i++) ok = (values[i] = ut_get(options, keys[i])) != NULL; + double concurrency = INFINITY, max_pending = INFINITY; + bool drop = false, strict = false; + ok = ok && ut_limit_option(values[0], "options.concurrency", 1, &concurrency) && + ut_limit_option(values[1], "options.maxPending", 0, &max_pending); + if (ok) { + const ScrDyn *overflow = values[2]; + bool queue = overflow->kind == SCR_DYN_UNDEF || + (overflow->kind == SCR_DYN_STR && overflow->v.str->len == 5 && !memcmp(overflow->v.str->data, "queue", 5)); + drop = overflow->kind == SCR_DYN_STR && overflow->v.str->len == 4 && !memcmp(overflow->v.str->data, "drop", 4); + if (!queue && !drop) { + scr_dyn_arg_value_fail("options.overflow", "must be one of: 'queue', 'drop'", overflow); + ok = false; + } + } + ok = ok && ut_signal_option(values[3], "throttle") && ut_boolean(values[4], "options.strict", &strict); + ScrDyn *result = NULL; + if (ok) { + UtState *s = ut_state_new(true, values[3]); + s->limit = count; + s->interval = ms; + s->concurrency = concurrency; + s->max_pending = max_pending; + s->drop = drop; + s->strict = strict; + result = ut_build(s, fn); + } + for (size_t i = 0; i < 5; i++) scr_dyn_release(values[i]); + return result; +} diff --git a/scripts/lowering-order.mjs b/scripts/lowering-order.mjs index 81611e6a4..6d68530cb 100644 --- a/scripts/lowering-order.mjs +++ b/scripts/lowering-order.mjs @@ -16,13 +16,16 @@ * [--seeds 1,2,3] [--jobs 8] [--entry ]... [--out ] * [--fail-on names|content] * + * --stride and --offset sample the top-level corpus programs by number, or by + * a hash of the name of a program without one. + * * Needs a built compiler (pnpm build). Writes /results.json and prints * a ranking of fields by the programs whose IR they change. With --fail-on, * it exits with status 1 when some program's IR differs between orders: * in any way (names), or beyond names and ids (content). */ import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { cpus, tmpdir } from "node:os"; import { basename, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -306,12 +309,25 @@ if (values["fail-on"] !== undefined && !["names", "content"].includes(values["fa const out = resolve(values.out); mkdirSync(out, { recursive: true }); const seeds = values.seeds.split(",").filter(Boolean); +/** A top-level corpus program's sampling key: its number, or, for a program + * named without one, a stable hash of its name (FNV-1a). */ +const programKey = (name) => + /^\d+-/.test(name) + ? Number.parseInt(name, 10) + : [...name].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0, 2166136261); +/** Numbered programs by number, then the named ones by name. */ +const programOrder = (a, b) => + /^\d+-/.test(a) !== /^\d+-/.test(b) + ? /^\d+-/.test(a) + ? -1 + : 1 + : programKey(a) - programKey(b) || (a < b ? -1 : a > b ? 1 : 0); const corpus = join(root, "tests/corpus"); const programs = [ ...readdirSync(corpus) - .filter((name) => /^\d+-.*\.ts$/.test(name)) - .filter((name) => Number.parseInt(name, 10) % Number(values.stride) === Number(values.offset)) - .sort((a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10)) + .filter((name) => /^[\w-]+\.ts$/.test(name) && statSync(join(corpus, name)).isFile()) + .filter((name) => programKey(name) % Number(values.stride) === Number(values.offset)) + .sort(programOrder) .slice(0, Number(values.limit)) .map((name) => join(corpus, name)), ...values.entry.map((entry) => resolve(entry)), diff --git a/scripts/refresh-cli-profile.mjs b/scripts/refresh-cli-profile.mjs index 1285b5503..dde4e21b9 100644 --- a/scripts/refresh-cli-profile.mjs +++ b/scripts/refresh-cli-profile.mjs @@ -7,8 +7,9 @@ * Builds an instrumented native compiler for this Linux host, runs it on the * training builds below, and merges the raw profiles it writes into one * sparse indexed profile. The training builds are tsc-ts (a cold `speed` - * build, then a rebuild after an edit), every seventh corpus program at the - * default optimization and a further slice in `dev`, and the runtime + * build, then a rebuild after an edit), every seventh top-level corpus + * program (by number, or by a hash of the name of a program without one) at + * the default optimization and a further slice in `dev`, and the runtime * benchmark workloads. Run it on a quiet Linux x64 host after changes that * move much compiler code; a stale profile only loses part of its gain. A * refresh takes about 11 minutes on 16 cores. @@ -126,16 +127,30 @@ async function pool(tasks) { return passed; } -const programNumber = (name) => Number.parseInt(name, 10); +/** A top-level corpus program's sampling key: its number, or, for a program + * named without one, a stable hash of its name (FNV-1a). */ +const programKey = (name) => + /^\d+-/.test(name) + ? Number.parseInt(name, 10) + : [...name].reduce((h, c) => Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0, 2166136261); +/** Numbered programs by number, then the named ones by name. */ +const programOrder = (a, b) => + /^\d+-/.test(a) !== /^\d+-/.test(b) + ? /^\d+-/.test(a) + ? -1 + : 1 + : programKey(a) - programKey(b) || (a < b ? -1 : a > b ? 1 : 0); const corpus = readdirSync(join(root, "tests/corpus")) - .filter((name) => /^\d+-.*\.ts$/.test(name)) - .sort((a, b) => programNumber(a) - programNumber(b)); + .filter( + (name) => /^[\w-]+\.ts$/.test(name) && statSync(join(root, "tests/corpus", name)).isFile(), + ) + .sort(programOrder); const training = [ ...corpus - .filter((name) => programNumber(name) % 7 === 0) + .filter((name) => programKey(name) % 7 === 0) .map((name) => ({ file: join(root, "tests/corpus", name), optimization: "release" })), ...corpus - .filter((name) => programNumber(name) % 14 === 7) + .filter((name) => programKey(name) % 14 === 7) .map((name) => ({ file: join(root, "tests/corpus", name), optimization: "dev" })), ...readdirSync(join(root, "benchmarks/runtime/workloads")) .filter((name) => name.endsWith(".ts")) diff --git a/tests/corpus/7520-util-format-runtime.ts b/tests/corpus/7520-util-format-runtime.ts new file mode 100644 index 000000000..d4a7f9ea6 --- /dev/null +++ b/tests/corpus/7520-util-format-runtime.ts @@ -0,0 +1,110 @@ +// util.format, util.formatWithOptions and console output with format +// strings known only at run time, spread arguments and options objects: +// every substitution, the argument cursor, %% and %c, missing and extra +// arguments, the space-joined form of strings without a '%', and Node's +// inspectOptions validation. +import * as util from "node:util"; + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +interface Row { + id: number; + tags: string[]; + nested: { deep: { deeper: { deepest: number } } }; +} + +const formats: string[] = [ + "%s and %s", + "%d|%i|%f", + "%j", + "%O / %o", + "100%% of %s", + "%c%s%c", + "no specifiers", + "trailing %", + "%x %s", + "%s", +]; +const row: Row = { id: 7, tags: ["a", "b"], nested: { deep: { deeper: { deepest: 1 } } } }; + +for (const format of formats) { + console.log(util.format(format, "first", 42, row)); + console.log(util.format(format, 3.5, "12.75")); + console.log(util.format(format)); +} + +function pick(i: number): string { + return ["%s=%d", "%j", "%O", "plain"][i % 4]!; +} +for (let i = 0; i < 4; i++) { + console.log(util.format(pick(i), new Point(1, 2), -0, [1, 2, 3])); + console.log(pick(i), new Map([["k", new Point(3, 4)]]), new Set([1n, 2n])); + console.error(pick(i), "to stderr", null, undefined); +} + +const label: string = process.argv.length > 99 ? "" : "count: %d of %s"; +console.log(label, 5, "five", "extra"); +console.log(util.format(label, 10n, Symbol("sym"))); +console.log(util.format(label, new Point(0, 0), [new Point(5, 6)])); +console.log(util.format(pick(0), { toString: "not a function" }, true)); + +// Arguments that are all strings, numbers, booleans, null or undefined. +const prim: string = process.argv.length > 99 ? "" : "%s|%d|%i|%f|%j|%O|%o|%c|%%"; +console.log(prim, -0, "12.5", -0, true, 'q"uote', "s", 3, "css", "rest", null, undefined); +console.log(prim, NaN, null, undefined, "0x10", Infinity, NaN, false); +console.log(util.format(prim, "a\nb", " 7 ", "-3.9e2", "", -Infinity, 'it\'s', 1e21)); +console.log(util.format(label, 2.5)); + +// A union first argument formats only when it holds a string. +const maybe: string | number = process.argv.length > 99 ? 1 : "%s/%s"; +const notString: string | number = process.argv.length > 99 ? "%s" : 9; +console.log(util.format(maybe, "u", "v")); +console.log(util.format(notString, "u", "v")); +console.log(maybe, "w", "x"); + +// An unknown first argument. +const loose: unknown = process.argv.length > 99 ? 0 : "%s-%i"; +console.log(util.format(loose, "a", "7.9")); +console.log(loose, "b", 3.2); + +// Spread arguments. +const values: unknown[] = ["%s + %s = %d", 1, 2, 3, "tail"]; +console.log(util.format(...values)); +console.log(util.format("%s|%s", ...values)); +console.log(...values); +console.log("prefix", ...new Set(["x", "y"]), "suffix"); +console.error(...[1, "two", [3]]); +console.log(util.format(...([] as string[]))); + +// formatWithOptions with an options object. +const deep = { a: { b: { c: { d: { e: 1 } } } } }; +const depth = process.argv.length > 99 ? 1 : 0; +console.log(util.formatWithOptions({ depth }, "%O", deep)); +console.log(util.formatWithOptions({ depth: null }, deep)); +console.log(util.formatWithOptions({ depth: undefined }, "x", deep)); +console.log(util.formatWithOptions({ depth: 0 }, "%s|%O", deep, deep)); +console.log(util.formatWithOptions({ compact: false }, "%s", "v", { a: 1, b: [1, 2] })); +console.log(util.formatWithOptions({ breakLength: 20 }, { alpha: 1, beta: 2, gamma: 3 })); +console.log(util.formatWithOptions({ sorted: true }, "%O", { b: 1, a: 2, c: { z: 1, y: 2 } })); +console.log(util.formatWithOptions({ colors: true }, "%s %d %O", "s", 2, 3), "|"); +console.log(util.formatWithOptions({ colors: true }, 1, "plain", null, undefined, true, 4n)); +const options: util.InspectOptions = { depth: 1, compact: 1 }; +console.log(util.formatWithOptions(options, "%O", deep)); +console.log(util.formatWithOptions(options, ...values)); +console.log(util.formatWithOptions([] as unknown as util.InspectOptions, "%s", 1)); +const invalid: unknown[] = [null, 1, "x", undefined, () => {}]; +for (const bad of invalid) { + try { + util.formatWithOptions(bad as util.InspectOptions, "a"); + } catch (error) { + const e = error as NodeJS.ErrnoException; + console.log(e.name, e.code, e.message); + } +} diff --git a/tests/corpus/7521-util-format-runtime.cjs b/tests/corpus/7521-util-format-runtime.cjs new file mode 100644 index 000000000..559d09243 --- /dev/null +++ b/tests/corpus/7521-util-format-runtime.cjs @@ -0,0 +1,64 @@ +// util.format, util.formatWithOptions and console output over untyped +// values: format strings held in variables and parameters, spread +// arguments, %s over objects with and without their own toString, %d and +// %i coercions, %j over toJSON, and options objects built at run time. +"use strict"; +const util = require("util"); + +function log(message, ...rest) { + console.log(message, ...rest); +} + +function formatAll(format, values) { + return util.format(format, ...values); +} + +const messages = ["%s: %d items", "%s", "%i%%", "%j", "plain text", 42]; +for (const message of messages) { + log(message, "cart", "3.7", { total: 9 }); + console.log(formatAll(message, ["a", 1n])); +} + +const withToString = { + name: "custom", + toString() { + return `<${this.name}>`; + }, +}; +const inherited = Object.create(withToString); +inherited.name = "inherited"; +const fmt = "%s / %s / %s"; +console.log(util.format(fmt, withToString, inherited, [1, [2, [3]]])); +console.log(fmt, new Date(0), new Error("boom").message, null, Symbol("s")); + +const numeric = "%d %i %f"; +console.log(util.format(numeric, "0x1f", "12.9px", "1e3")); +console.log(util.format(numeric, { valueOf: () => 5 }, true, [])); +console.log(util.format(numeric, 7n, 8n, 9)); + +const json = "%j|%j|%j"; +const circular = { name: "loop" }; +circular.self = circular; +console.log(util.format(json, { toJSON: () => ({ replaced: 1 }) }, circular, undefined)); + +const options = {}; +options.depth = 0; +options.breakLength = 40; +console.log(util.formatWithOptions(options, "%O", { a: { b: 1 } }, ["x", { y: { z: 2 } }])); +options.depth = null; +options.unknownOption = true; +console.log(util.formatWithOptions(options, { a: { b: { c: { d: 1 } } } })); +console.log(util.formatWithOptions({ compact: 1 }, ...[{ k: [1, 2] }, "end"])); +for (const bad of [null, 0, "options", () => {}]) { + try { + util.formatWithOptions(bad, "x"); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +const args = ["%s=%s", "key"]; +args.push("value", "rest"); +console.log(...args); +console.warn(...args); +console.log(util.format(...args.slice(1))); diff --git a/tests/corpus/7522-promisify-untyped.cjs b/tests/corpus/7522-promisify-untyped.cjs new file mode 100644 index 000000000..2e1476eac --- /dev/null +++ b/tests/corpus/7522-promisify-untyped.cjs @@ -0,0 +1,64 @@ +// util.promisify over JavaScript functions whose types are not known: +// arrows, function expressions, methods and functions chosen at run time. +// The callback's error rejects and its first value fulfills, a +// synchronous throw rejects, the call's receiver reaches the function, +// the wrapper keeps the original's name and length, and arguments that +// are not functions throw Node's ERR_INVALID_ARG_TYPE. +"use strict"; +const util = require("util"); + +function pick(kind) { + if (kind === "double") return (value, callback) => setImmediate(() => callback(null, value * 2)); + if (kind === "fail") return function failing(callback) { callback(new RangeError("failed")); }; + if (kind === "throw") return () => { throw new TypeError("thrown"); }; + return null; +} + +const store = { + prefix: "item", + read(key, callback) { + setTimeout(() => callback(null, `${this.prefix}:${key}`, "ignored"), 1); + }, +}; + +async function main() { + const double = util.promisify(pick("double")); + console.log("double:", await double(21), double.name, double.length); + try { + await util.promisify(pick("fail"))(); + } catch (error) { + console.log("rejected:", error.name, error.message); + } + try { + await util.promisify(pick("throw"))(); + } catch (error) { + console.log("sync:", error.name, error.message); + } + const read = util.promisify(store.read); + console.log("unbound:", await read.call(store, "a")); + store.readAsync = read; + console.log("method:", await store.readAsync("b")); + const bound = util.promisify(store.read.bind({ prefix: "bound" })); + console.log("bound:", await bound("c")); + const named = util.promisify(function lookup(a, b, callback) { + callback(null, [a, b]); + }); + console.log("named:", named.name, named.length, await named(1, 2)); + const values = []; + for (const n of [1, 2, 3]) values.push(await double(n)); + console.log("each:", values); + for (const value of [pick("none"), 42, "fn", {}, undefined]) { + try { + util.promisify(value); + } catch (error) { + console.log(error.name, error.code, error.message); + } + } + try { + util.promisify(); + } catch (error) { + console.log(error.code, error.message); + } +} + +main(); diff --git a/tests/corpus/7523-promisify-untyped.ts b/tests/corpus/7523-promisify-untyped.ts new file mode 100644 index 000000000..65d1d01c1 --- /dev/null +++ b/tests/corpus/7523-promisify-untyped.ts @@ -0,0 +1,38 @@ +// util.promisify over untyped functions in TypeScript: values typed `any` +// and functions whose parameters are `any` promisify at run time, and +// their promises settle with the callback's value (not the type of a +// builtin target's promisified form). +import { promisify } from "node:util"; + +const loose: any = (value: number, callback: (error: Error | null, result?: number) => void) => + callback(null, value + 1); +const looseAsync = promisify(loose); + +function legacy(a: any, b: any, callback: any): void { + setImmediate(() => (b === 0 ? callback(new RangeError("divide by zero")) : callback(null, a / b))); +} +const divide = promisify(legacy); + +const handlers: Record = { + echo: (text: string, callback: (error: null, value: string) => void) => callback(null, text), + later: (callback: (error: null, value: string[]) => void) => + setTimeout(() => callback(null, ["a", "b"]), 1), +}; + +async function main(): Promise { + console.log("loose:", await looseAsync(1)); + looseAsync(2).then((value: unknown) => console.log("then:", value)); + console.log("divide:", await divide(10, 4)); + try { + await divide(1, 0); + } catch (error) { + console.log("rejected:", (error as Error).message); + } + for (const name of ["echo", "later"]) { + const fn = promisify(handlers[name]); + console.log(`${name}:`, name === "echo" ? await fn("hi") : await fn()); + } + console.log("name:", divide.name, "length:", divide.length); +} + +main(); diff --git a/tests/corpus/7524-inspect-custom.ts b/tests/corpus/7524-inspect-custom.ts new file mode 100644 index 000000000..642ce75a9 --- /dev/null +++ b/tests/corpus/7524-inspect-custom.ts @@ -0,0 +1,91 @@ +// [util.inspect.custom] methods on classes: the method's string replaces +// the instance in util.inspect, console output and util.format, with +// Node's depth argument (negative past the depth limit, null for depth: +// null), options object and util.inspect; a multi-line result indents at +// its position, a returned value renders in the instance's place, and +// console.dir, customInspect: false, String() and JSON skip the method. +import * as util from "node:util"; +import { inspect } from "node:util"; + +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + [inspect.custom](): string { + return `Money(${(this.cents / 100).toFixed(2)})`; + } +} + +class Probe { + label: string; + constructor(label: string) { + this.label = label; + } + [util.inspect.custom](depth: number | null, options: util.InspectOptionsStylized): string { + const keys = Object.keys(options).join(","); + return `Probe<${this.label} depth=${depth} optionsDepth=${options.depth} compact=${options.compact}> ${keys}`; + } +} + +class Lines { + [inspect.custom](): string { + return "first line\nsecond line"; + } +} + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + [inspect.custom](): { x: number; y: number; sum: number } { + return { x: this.x, y: this.y, sum: this.x + this.y }; + } +} + +class Wrapper { + inner: { values: number[]; nested: { deep: { deeper: number } } }; + constructor() { + this.inner = { values: [1, 2, 3], nested: { deep: { deeper: 4 } } }; + } + [inspect.custom](depth: number, options: util.InspectOptionsStylized, show: typeof inspect): string { + if (depth < 0) return options.stylize("[Wrapper]", "special"); + return `Wrapper ${show(this.inner, { ...options, depth: options.depth == null ? null : depth - 1 })}`; + } +} + +class Base { + name = "base"; + [inspect.custom](): string { + return `Custom(${this.name})`; + } +} +class Derived extends Base { + extra = 1; +} + +console.log(util.inspect(new Money(1234))); +console.log(new Money(5), [new Money(1), new Money(2)]); +console.log(util.inspect({ wallet: { cash: new Money(99) } })); +console.log(util.inspect(new Probe("top"))); +console.log(util.inspect({ a: { b: new Probe("nested") } })); +console.log(util.inspect([new Probe("past depth")], { depth: 0 })); +console.log(util.inspect(new Probe("unbounded"), { depth: null })); +console.log(util.inspect(new Probe("infinite"), { depth: Infinity })); +console.log(util.inspect([new Probe("undefined")], { depth: undefined })); +console.log(util.inspect(new Probe("compact"), { compact: false, breakLength: 40 })); +console.log(util.inspect({ key: new Lines(), list: [new Lines()] })); +console.log(util.inspect(new Map([["k", new Lines()]]))); +console.log(util.inspect(new Point(1, 2)), util.inspect({ p: new Point(3, 4) })); +console.log(util.inspect(new Wrapper()), util.inspect({ w: new Wrapper() }, { depth: 0 })); +console.log(util.inspect(new Derived()), util.inspect([new Base(), new Derived()])); +console.log(util.format("%s | %O", new Money(10), new Money(20))); +console.log("%s and %d", new Money(1), 2); +console.log(util.inspect(new Money(7), { customInspect: false })); +console.dir(new Money(8)); +console.log(String(new Money(9)), JSON.stringify(new Money(10))); +const values: unknown[] = [new Money(11), { money: new Money(12) }]; +console.log(values, util.inspect(values[1]), [new Lines(), values[0]]); diff --git a/tests/corpus/7525-inspect-custom.cjs b/tests/corpus/7525-inspect-custom.cjs new file mode 100644 index 000000000..9c7302fac --- /dev/null +++ b/tests/corpus/7525-inspect-custom.cjs @@ -0,0 +1,75 @@ +// [util.inspect.custom] in JavaScript: methods on classes and object +// literals (also keyed by Symbol.for('nodejs.util.inspect.custom')), the +// CommonJS call form (0, util_1.inspect)(value), results that are the +// object itself or another value, a non-function member under the key, +// and the options and util.inspect a method receives. +"use strict"; +const util_1 = require("util"); + +class Temperature { + constructor(degrees) { + this.degrees = degrees; + } + [util_1.inspect.custom](depth, options, inspect) { + return `Temperature<${this.degrees}> ${typeof options.stylize} ${inspect === util_1.inspect}`; + } +} + +const literal = { + id: 1, + [util_1.inspect.custom](depth) { + return `literal at depth ${depth}`; + }, +}; +const registered = { + [Symbol.for("nodejs.util.inspect.custom")]() { + return { replaced: [1, 2], nested: { x: 1 } }; + }, +}; +const self = { + v: 1, + [util_1.inspect.custom]() { + return this; + }, +}; +const notFunction = { x: 1, [util_1.inspect.custom]: "not a function" }; +const styled = { + [util_1.inspect.custom](depth, options) { + return options.stylize("styled", "special") + options.stylize(5, "number"); + }, +}; +const nesting = { + data: { a: { b: { c: 1 } } }, + [util_1.inspect.custom](depth, options, inspect) { + return `nesting ${inspect(this.data, { ...options, depth: 0 })}`; + }, +}; + +console.log((0, util_1.inspect)(new Temperature(21))); +console.log((0, util_1.inspect)({ t: new Temperature(22), list: [literal] })); +console.log(util_1.inspect([[literal]], { depth: 0 })); +console.log(util_1.inspect(registered), util_1.inspect({ r: registered })); +console.log(util_1.inspect(self), util_1.inspect(notFunction)); +console.log(util_1.inspect(styled), JSON.stringify(util_1.inspect(styled, { colors: true }))); +console.log(util_1.inspect(nesting), util_1.inspect({ n: nesting })); +console.log(literal, [registered]); +console.log((0, util_1.format)("%s / %O", literal, literal)); +console.log(util_1.inspect(literal, { customInspect: false })); +console.dir(literal); +console.log(util_1.inspect.custom === Symbol.for("nodejs.util.inspect.custom"), util_1.inspect.custom); + +const throwing = { + [util_1.inspect.custom]() { + throw new RangeError("from custom"); + }, +}; +try { + console.log([throwing]); +} catch (error) { + console.log("caught", error.name, error.message); +} +try { + util_1.inspect({ t: throwing }); +} catch (error) { + console.log("caught", error.message); +} diff --git a/tests/corpus/7526-inspect-options-runtime.ts b/tests/corpus/7526-inspect-options-runtime.ts new file mode 100644 index 000000000..b9f0c61d7 --- /dev/null +++ b/tests/corpus/7526-inspect-options-runtime.ts @@ -0,0 +1,62 @@ +// util.inspect with colors, option values and options objects known only +// at run time, and the legacy inspect(value, showHidden, depth, colors) +// form: Node's ANSI styles per value kind, color-aware line breaking and +// grouping, runtime depth and layout options, and colored output from +// util.formatWithOptions. +import * as util from "node:util"; + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +const sparse: number[] = [1, 2, 3]; +sparse.length = 5; +const values: unknown[] = [ + 1, + -0, + "text", + "it's", + null, + undefined, + true, + 10n, + Symbol("sym"), + [1, "two", [3, [4, [5]]]], + { a: 1, "b-c": "d", nested: { deep: { deeper: { deepest: 1 } } } }, + new Map([["k", { v: 1 }]]), + new Set(["s", 2]), + new Date(0), + new Uint8Array([1, 2, 3]), + Buffer.from("hi"), + function named() {}, + new Point(1, 2), + sparse, + Object.create(null), + Array.from({ length: 30 }, (_, i) => i * 37), + "a long string with a newline\nand more text than fits on one line of eighty characters", +]; +for (const value of values) { + console.log(JSON.stringify(util.inspect(value, { colors: true }))); +} +const depth = process.argv.length > 99 ? 5 : 1; +const breakLength = process.argv.length > 99 ? 80 : 30; +console.log(util.inspect(values[10], { depth })); +console.log(util.inspect(values[10], { depth: depth - 1, breakLength })); +console.log(util.inspect(values[20], { breakLength, compact: depth })); +console.log(util.inspect({ b: 2, a: 1 }, { sorted: depth > 0 })); +console.log(util.inspect(values[10], { depth: undefined })); +const options: util.InspectOptions = { depth: 0, colors: true }; +console.log(JSON.stringify(util.inspect(values[10], options))); +options.colors = false; +console.log(util.inspect(values[10], options)); +console.log(util.inspect(values[10], false, 0)); +console.log(util.inspect(values[10], false, null, false)); +console.log(JSON.stringify(util.inspect(values[11], false, 1, true))); +console.log(util.inspect(values[10], undefined, depth)); +console.log(JSON.stringify(util.formatWithOptions({ colors: true }, "%s %O", "x", values[12]))); +console.log(JSON.stringify(util.formatWithOptions({ colors: true }, values[10], values[9]))); diff --git a/tests/corpus/7536-inspect-custom-errors.ts b/tests/corpus/7536-inspect-custom-errors.ts new file mode 100644 index 000000000..affdd587b --- /dev/null +++ b/tests/corpus/7536-inspect-custom-errors.ts @@ -0,0 +1,23 @@ +// [util.inspect.custom] on Error subclasses: console.log, util.inspect and +// %O call the method for typed values, values held as unknown and errors +// inside containers; a subclass without its own method inherits it, and +// errors without one print as before. +import * as util from "node:util"; + +class Tagged extends Error { + [util.inspect.custom](depth: number): string { + return `Tagged<${this.message}> at depth ${depth}`; + } +} +class Nested extends Tagged {} +class Plain extends Error {} + +const tagged = new Tagged("a"); +console.log(tagged); +console.log(util.inspect(tagged), util.format("%O", new Nested("b"))); +const held: unknown = new Nested("c"); +console.log(held); +const list: unknown[] = [tagged, { inner: new Tagged("d") }]; +console.log(list); +const plain: unknown = new Plain("plain"); +console.log(String(plain), plain instanceof Error); diff --git a/tests/corpus/7537-inspect-custom-errors.cjs b/tests/corpus/7537-inspect-custom-errors.cjs new file mode 100644 index 000000000..3d17f985a --- /dev/null +++ b/tests/corpus/7537-inspect-custom-errors.cjs @@ -0,0 +1,26 @@ +// An Effect-style error class in JavaScript: a method keyed by +// Symbol.for("nodejs.util.inspect.custom") on an Error subclass, called by +// console.log and util.inspect for direct, caught and contained values, +// and the class still constructs and throws when nothing inspects it. +"use strict"; +const util = require("util"); +const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom"); + +class YieldableError extends Error { + toJSON() { + return { _tag: "YieldableError", message: this.message }; + } + [NodeInspectSymbol]() { + return `YieldableError: ${this.message}`; + } +} + +const direct = new YieldableError("direct"); +console.log(direct, direct instanceof Error, JSON.stringify(direct)); +try { + throw new YieldableError("boom"); +} catch (e) { + console.log(e instanceof YieldableError, e.message); + console.log(e); + console.log(util.inspect([e]), util.inspect({ e })); +} diff --git a/tests/corpus/7538-inspect-custom-prototypes.mjs b/tests/corpus/7538-inspect-custom-prototypes.mjs new file mode 100644 index 000000000..7a4bfb1a4 --- /dev/null +++ b/tests/corpus/7538-inspect-custom-prototypes.mjs @@ -0,0 +1,44 @@ +// [util.inspect.custom] methods the runtime inspect walk calls: a class +// method returning an object (Effect's `return this.toJSON()`) that no +// typed code inspects, and a method an Effect-style class inherits from a +// constructor function's prototype object. +const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom"); + +class Exit { + constructor(value) { + this.value = value; + } + toJSON() { + return { _id: "Exit", value: this.value }; + } + [NodeInspectSymbol]() { + return this.toJSON(); + } +} +const exits = [new Exit(1), new Exit("two")]; +console.log(JSON.stringify(exits)); +console.log(exits); + +const CommitPrototype = { + toJSON() { + return { _id: "Commit" }; + }, + [NodeInspectSymbol]() { + return this.toJSON(); + }, +}; +const Base = (function () { + function Base() {} + Base.prototype = CommitPrototype; + return Base; +})(); +class Effectable extends Base {} +class RefImpl extends Effectable { + constructor(ref) { + super(); + this.ref = ref; + } +} +const ref = new RefImpl(3); +console.log(ref.ref); +console.log(ref, [ref]); diff --git a/tests/corpus/7700-util-deprecated-helpers.cjs b/tests/corpus/7700-util-deprecated-helpers.cjs new file mode 100644 index 000000000..39ec5d65f --- /dev/null +++ b/tests/corpus/7700-util-deprecated-helpers.cjs @@ -0,0 +1,30 @@ +// util.isArray and util._extend, the deprecated helpers: Node's answers +// (_extend assigns from the last own key to the first and leaves +// non-object sources alone), and DEP0044/DEP0060 once each on the next +// tick, shared with util.deprecate's codes. The program re-runs itself and +// prints the child's output with process ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const util = require("util"); +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code)); + console.log(util.isArray([1]), util.isArray("x"), util.isArray({ length: 0 }), util.isArray()); + console.log(util._extend({ a: 1 }, { b: 2, c: 3 })); + console.log(Object.keys(util._extend({}, { x: 1, y: 2, z: 3 }))); + console.log(util._extend({ a: 1 }, null), util._extend({ a: 1 }, "str"), util._extend({ a: 1 }, () => 1)); + console.log(util._extend([0], [5, 6]), util._extend({ k: 1 })); + const t = { keep: true }; + console.log(util._extend(t, { keep: false }) === t, t); + try { util._extend(null, { a: 1 }); } catch (e) { console.log(e.name, e.message); } + console.log(util._extend(null, {})); + util.isArray([]); + const dep = util.deprecate(() => "wrapped", "a custom DEP0060", "DEP0060"); + console.log(dep(), "sync end"); +} +if (process.argv[2] === "child") child(); +else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7701-util-deprecated-helpers.ts b/tests/corpus/7701-util-deprecated-helpers.ts new file mode 100644 index 000000000..badc8ec05 --- /dev/null +++ b/tests/corpus/7701-util-deprecated-helpers.ts @@ -0,0 +1,21 @@ +// util.isArray and util._extend over typed values: isArray answers by the +// runtime value, and _extend writes into the typed target and answers the +// target itself. +import * as util from "node:util"; + +process.noDeprecation = true; +const nums: number[] = [1, 2]; +const text: string = "x"; +const anything: unknown = [3]; +console.log(util.isArray(nums), util.isArray(text), util.isArray(new Map()), util.isArray(anything)); + +interface Opts { + a: number; + b?: string; +} +const base: Opts = { a: 1 }; +const merged = util._extend(base, { b: "two" }); +console.log(merged, merged === base, base.b); +const plain = util._extend({}, { x: 1, y: 2 }); +console.log(plain, Object.keys(plain)); +console.log(util._extend({ keep: 1 }, null), util._extend({ keep: 2 }, undefined)); diff --git a/tests/corpus/7702-util-signal-exit-code.cjs b/tests/corpus/7702-util-signal-exit-code.cjs new file mode 100644 index 000000000..000a342df --- /dev/null +++ b/tests/corpus/7702-util-signal-exit-code.cjs @@ -0,0 +1,17 @@ +// util.convertProcessSignalToExitCode: 128 plus the os.constants.signals +// number, aliases included, and ERR_INVALID_ARG_VALUE listing every signal +// name for unknown names, other cases and non-strings. +"use strict"; +const util = require("util"); +const os = require("os"); + +for (const v of ["SIGKILL", "SIGTERM", "SIGINT", "SIGHUP", "SIGSYS", "SIGIOT", "nope", "sigint", 9, undefined, null]) { + try { + console.log(util.convertProcessSignalToExitCode(v)); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} +console.log(Object.keys(os.constants.signals).every((name) => util.convertProcessSignalToExitCode(name) === 128 + os.constants.signals[name])); +const convert = util.convertProcessSignalToExitCode; +console.log(convert("SIGPIPE"), typeof convert); diff --git a/tests/corpus/7703-util-aborted.cjs b/tests/corpus/7703-util-aborted.cjs new file mode 100644 index 000000000..66a28c1cd --- /dev/null +++ b/tests/corpus/7703-util-aborted.cjs @@ -0,0 +1,17 @@ +// util.aborted(signal, resource): a promise that rejects with Node's +// ERR_INVALID_ARG_TYPE for a non-AbortSignal or a non-object resource, +// resolves with undefined for an aborted signal and with the abort event +// later, in Node's microtask order. +const util = require("util"); +const ac = new AbortController(); +const p = util.aborted(ac.signal, {}); +console.log(p instanceof Promise); +p.then(/** @param {any} v */ (v) => console.log("later", typeof v, v && v.type, v instanceof Event)); +const done = new AbortController(); done.abort(); +util.aborted(done.signal, {}).then((v) => console.log("already", v)); +util.aborted({}, {}).catch((e) => console.log(e.name, e.code, e.message)); +util.aborted(ac.signal, 1).catch((e) => console.log(e.name, e.code, e.message)); +util.aborted(ac.signal, null).catch((e) => console.log(e.name, e.code, e.message)); +util.aborted(ac.signal, () => {}).then(() => console.log("fn resource ok")); +util.aborted(undefined).catch((e) => console.log(e.name, e.code, e.message)); +setTimeout(() => ac.abort("why"), 1); diff --git a/tests/corpus/7704-util-aborted.ts b/tests/corpus/7704-util-aborted.ts new file mode 100644 index 000000000..18af30182 --- /dev/null +++ b/tests/corpus/7704-util-aborted.ts @@ -0,0 +1,21 @@ +// util.aborted in TypeScript: the listener resists stopImmediatePropagation, +// and the promise settles two microtask turns after the signal's own +// promise, as an async function's result adopts it. +import { aborted } from "node:util"; +const ac = new AbortController(); +ac.signal.addEventListener("abort", (e) => { + console.log("first listener stops propagation"); + e.stopImmediatePropagation(); +}); +const resource = { name: "job" }; +const order: string[] = []; +aborted(ac.signal, resource).then(() => order.push("aborted")); +Promise.resolve().then(() => order.push("tick 1")).then(() => order.push("tick 2")); +ac.abort(); +setTimeout(() => console.log(order), 0); +const done = new AbortController(); +done.abort(); +const seq: string[] = []; +aborted(done.signal, resource).then(() => seq.push("already")); +Promise.resolve().then(() => seq.push("a")).then(() => seq.push("b")).then(() => seq.push("c")); +setTimeout(() => console.log(seq), 0); diff --git a/tests/corpus/7705-util-diff.cjs b/tests/corpus/7705-util-diff.cjs new file mode 100644 index 000000000..489586a3b --- /dev/null +++ b/tests/corpus/7705-util-diff.cjs @@ -0,0 +1,14 @@ +// util.diff over strings (one entry per UTF-16 code unit, astral pairs as +// two) and arrays of strings: Node's Myers diff entries in order, identical +// values answering [] before validation, and ERR_INVALID_ARG_TYPE naming +// the argument or the array element. +const util = require("util"); +const show = (a, e) => { + try { console.log(JSON.stringify(util.diff(a, e))); } catch (err) { console.log(err.name, err.code, err.message); } +}; +show("abc", "abd"); show("kitten", "sitting"); show("", "x"); show("same", "same"); show("", ""); +show(["a", "b", "c"], ["a", "c", "d"]); show(["x"], []); show([], ["y", "z"]); show([], []); +const arr = ["p"]; show(arr, arr); show(1, 1); show(1, 2); show("a", 2); show(["a", 1], ["a"]); show(["a"], [, "b"]); +show(null, "x"); show("x", undefined); show("héllo😀", "hello😀"); show({}, "x"); +show("the quick brown fox", "the quack brown box"); +console.log(util.diff(["line 1", "line 2"], ["line 1", "line 3"])); diff --git a/tests/corpus/7706-util-diff.ts b/tests/corpus/7706-util-diff.ts new file mode 100644 index 000000000..7effff734 --- /dev/null +++ b/tests/corpus/7706-util-diff.ts @@ -0,0 +1,13 @@ +// util.diff with typed arrays of strings and DiffEntry tuples. +import { diff, type DiffEntry } from "node:util"; +const before: string[] = ["alpha", "beta", "gamma", "delta"]; +const after: string[] = ["alpha", "gamma", "delta", "epsilon"]; +const entries: DiffEntry[] = diff(before, after); +for (const [op, value] of entries) console.log(op === 0 ? " " : op === 1 ? "-" : "+", value); +const counts = { kept: 0, actual: 0, expected: 0 }; +for (const entry of diff("abcdef", "azcdxf")) { + if (entry[0] === 0) counts.kept++; + else if (entry[0] === 1) counts.actual++; + else counts.expected++; +} +console.log(counts, diff("x", "x").length); diff --git a/tests/corpus/7707-util-mime-type.cjs b/tests/corpus/7707-util-mime-type.cjs new file mode 100644 index 000000000..e860d1d30 --- /dev/null +++ b/tests/corpus/7707-util-mime-type.cjs @@ -0,0 +1,87 @@ +// util.MIMEType and util.MIMEParams: parsing, serialization, setters, +// parameter maps, iteration, errors, inspect, JSON and %s formatting. +const util = require("util"); +const { MIMEType, MIMEParams, inspect, format } = util; +const show = (label, run) => { + try { console.log(label, run()); } catch (e) { console.log(label, e.name, e.code, e.message); } +}; +const inputs = [ + "text/javascript", " TEXT/HTML ; Charset=UTF-8 ", "application/json;a=1;b=2;a=3", "text/plain;x", "text/plain;x=", + 'text/plain;q="a\\"b\\\\c";r="unterminated', 'text/plain;q="ends with \\', "text/plain;;;k=v;;", "text/plain; k = v", + "text/plain;k=\"\"", "text/plain;é=1;v=é", "text/plain;k=vĀ", "image/svg+xml;charset=utf-8", + "text", "text/", "/plain", "te xt/plain", "text/pla in", "té/x", "", " \t\r\n", +]; +for (const input of inputs) { + show(JSON.stringify(input), () => { + const m = new MIMEType(input); + return [m.type, m.subtype, m.essence, String(m), m.params.toString(), [...m.params]]; + }); +} +show("parse ok", () => String(MIMEType.parse("Text/Plain;A=B"))); +show("parse bad", () => MIMEType.parse("nope")); +show("illegal", () => new MIMEType("text/plain", 1)); +show("null second", () => String(new MIMEType("text/plain", null))); +show("object input", () => String(new MIMEType({ toString: () => "a/b;c=d" }))); +const m = new MIMEType("text/plain;charset=ascii"); +m.type = "Application"; m.subtype = "JSON"; +show("setters", () => String(m)); +show("bad type", () => { m.type = "a b"; }); +show("bad subtype", () => { m.subtype = ""; }); +const p = m.params; +show("same params", () => p === m.params); +p.set("Charset", "utf-8"); p.set("q", "1 2"); p.set("empty", ""); +show("params", () => [String(m), p.get("CHARSET"), p.get("none"), p.has("q"), p.toJSON()]); +show("bad name", () => p.set("a b", "x")); +show("bad value", () => p.set("ok", "Ā")); +p.delete("q"); +show("after delete", () => [...p.keys()].join(",")); +show("entries", () => [...p.entries()]); +show("values", () => [...p.values()]); +const fresh = new MIMEParams(); +fresh.set("b", "2"); fresh.set("a", "1"); fresh.set("b", "3"); +show("fresh", () => [String(fresh), [...fresh]]); +show("json", () => JSON.stringify({ m, p: fresh })); +show("instanceof", () => [m instanceof MIMEType, p instanceof MIMEParams, p instanceof MIMEType, {} instanceof MIMEType]); +console.log(m, p); +console.log(inspect({ nested: { m } }, { depth: 0 })); +const live = new MIMEParams(); +live.set("a", "1"); live.set("b", "2"); live.set("c", "3"); +const seen = []; +for (const [k] of live) { seen.push(k); if (k === "a") { live.delete("b"); live.set("d", "4"); } } +console.log(seen, `${m}`); + +{ + show("number input", () => new MIMEType(5)); + show("undefined input", () => new MIMEType()); + const m = new MIMEType("a/b"); + const p = m.params; + p.set(1, 2); p.set("semi", "x;y"); p.set("quote", 'say "hi"'); p.set("back", "a\\b"); p.set("space", "a b"); + show("quoted out", () => p.toString()); + show("get coerced", () => [p.get(1), p.has(1), p.get(), p.has()]); + p.delete(1); + show("deleted", () => [...p.keys()]); + show("typeof", () => [typeof m, typeof p, typeof MIMEType]); + show("keys", () => [Object.keys(m), Object.keys(p), m.foo, p.size]); + show("upper set", () => { m.type = "TEXT"; m.subtype = "X-Thing+JSON"; return m.essence; }); + show("empty type", () => { m.type = ""; }); + show("type with slash", () => { m.type = "a/b"; }); + show("set name empty", () => p.set("", "v")); + show("format", () => format("%s|%j|%o", m, m, p)); + show("inspect arr", () => inspect([m, p, { m }])); + show("concat", () => "x" + m + p); + show("parse throws?", () => [MIMEType.parse("a/b;c=\"d"), MIMEType.parse("")]); + show("parse coerces", () => String(MIMEType.parse({ toString() { return "x/y;z=1"; } }))); + show("params iter", () => { const it = p[Symbol.iterator](); return [it.next(), it.next().value]; }); + show("entries next", () => { const it = p.entries(); return it.next(); }); + const bare = new MIMEType("a/b"); + console.log(Object.keys(bare), bare.foo, bare.params.size); + console.log(format("%s|%s", bare, bare.params), "%s", new MIMEType("c/d;e=f")); + // Every argument evaluates before the constructor or parse runs. + const order = []; + const note = (label) => { order.push(label); return label; }; + const coerced = { toString() { order.push("coerce"); return "a/b"; } }; + show("surplus, then coercion", () => { new MIMEType(coerced, undefined, note("third")); return order.splice(0).join(","); }); + show("surplus, then parse error", () => { try { new MIMEType("bad", undefined, note("x")); } catch (e) { order.push(e.code); } return order.splice(0).join(","); }); + show("parse surplus", () => { MIMEType.parse(coerced, note("second")); return order.splice(0).join(","); }); + show("params surplus", () => { new MIMEParams(note("p")); return order.splice(0).join(","); }); +} diff --git a/tests/corpus/7708-util-mime-type.ts b/tests/corpus/7708-util-mime-type.ts new file mode 100644 index 000000000..a3db92555 --- /dev/null +++ b/tests/corpus/7708-util-mime-type.ts @@ -0,0 +1,44 @@ +// util.MIMEType and util.MIMEParams through their TypeScript types. +import { MIMEType, MIMEParams, format, inspect } from "node:util"; + +function describe(m: MIMEType): string { + return `${m.essence} [${m.params.toString()}]`; +} + +const types: MIMEType[] = []; +for (const s of ["text/html;charset=utf-8", "application/ld+json", "multipart/form-data; boundary=abc"]) { + types.push(new MIMEType(s)); +} +for (const m of types) console.log(describe(m), m.type.length, m.subtype.toUpperCase()); + +const parsed: MIMEType | null = MIMEType.parse("bad"); +console.log(parsed === null, MIMEType.parse("a/b")?.essence); + +const params: MIMEParams = types[0].params; +params.set("level", "1"); +let joined = ""; +for (const [name, value] of params) joined += `${name}:${value} `; +console.log(joined.trim(), params.has("level"), params.get("missing") ?? "none"); +const keys: string[] = [...params.keys()]; +const values: string[] = Array.from(params.values()); +console.log(keys, values, JSON.stringify(types[0]), JSON.stringify(params)); + +try { + new MIMEType("nope"); +} catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(err instanceof TypeError, err.code, err.message); +} +try { + types[1].type = "bad type"; +} catch (e) { + console.log((e as Error).message); +} +types[1].subtype = "xml"; +console.log(String(types[1]), types[1] instanceof MIMEType, inspect(types[1]), inspect(params)); +const empty = new MIMEParams(); +console.log(empty.toString() === "", [...empty].length, `${types[2]}`); +const seeded = new Map(params); +const pairs: [string, string][] = [...params.entries()]; +console.log(seeded.get("level"), seeded.size, pairs, [...empty.values()]); +console.log(format("%s / %s", types[0], params), `${params}`); diff --git a/tests/corpus/7709-function-own-properties.cjs b/tests/corpus/7709-function-own-properties.cjs new file mode 100644 index 000000000..7bf6fc77f --- /dev/null +++ b/tests/corpus/7709-function-own-properties.cjs @@ -0,0 +1,22 @@ +// Function values with own properties: inspect shows their own name and +// enumerable members. +const util = require("node:util"); + +function named() {} +named.x = 1; +named.list = [1, 2]; +console.log(named); + +const dep = util.deprecate(function inner(a, b) {}, "unused"); +dep.y = 2; +console.log(dep, Object.keys(dep)); +/** @type {any} */ +const held = dep; +console.log({ g: held }, [held]); +console.log({ a: { b: { c: { f: held } } } }); +Object.defineProperty(dep, "name", { value: "renamed" }); +Object.defineProperty(dep, "hidden", { value: 3, enumerable: false }); +console.log(dep, util.inspect(held, { compact: false })); + +const plain = util.callbackify(async () => 1); +console.log(plain); diff --git a/tests/corpus/7710-util-debounce.cjs b/tests/corpus/7710-util-debounce.cjs new file mode 100644 index 000000000..5ae49004d --- /dev/null +++ b/tests/corpus/7710-util-debounce.cjs @@ -0,0 +1,109 @@ +// util.debounce: trailing and leading calls, flush, cancel, rejectOnCancel, +// receivers, re-entry, options.signal, validation, the returned +// function's own members, and unref'd timers at exit. +const util = require("node:util"); + +const settle = (label, p) => p.then((v) => console.log(label, "ok", v), (e) => console.log(label, "err", e.name, e.message, e.cause instanceof Error ? e.cause.message : e.cause)); + +async function main() { + // Trailing calls collapse onto the last arguments. + let calls = 0; + const add = util.debounce(function (a, b) { calls++; return [this === add, a + b]; }, 20); + const p1 = add(1, 2), p2 = add(3, 4), p3 = add(5, 6); + console.log(add.pendingCount, add.pending === p3, p1 === p2); + console.log(await p1, await p2, await p3, calls, add.pendingCount, add.pending); + + // leading: the first call runs at once, later ones trail. + const lead = util.debounce((x) => x * 10, 15, { leading: true }); + const l1 = lead(1), l2 = lead(2); + console.log(lead.pendingCount); + console.log(await l1, await l2); + + // flush runs the pending call now; cancel rejects it. + const f = util.debounce((x) => `ran ${x}`, 1000); + const f1 = f("a"); + f.flush(); + console.log(await f1, f.pendingCount); + f.flush(); + const c1 = f("b"); + f.cancel(); + await settle("cancel", c1); + const c2 = f("c"); + f.cancel(new Error("stop")); + await settle("cancel-reason", c2); + f.cancel(); + + // rejectOnCancel rejects superseded calls. + const r = util.debounce((x) => x, 10, { rejectOnCancel: true }); + const r1 = r(1), r2 = r(2); + await settle("superseded", r1); + await settle("latest", r2); + + // A throwing fn rejects every waiting call. + const t = util.debounce(() => { throw new TypeError("bad"); }, 5); + const t1 = t(), t2 = t(); + await settle("throw1", t1); + await settle("throw2", t2); + + // ref and unref answer the function. + console.log(f.ref() === f, f.unref() === f, f.ref() === f); + + // options.signal: aborting rejects pending calls and every later call. + const ac = new AbortController(); + const s = util.debounce((x) => x, 1000, { signal: ac.signal }); + const s1 = s(1); + ac.abort("reason!"); + await settle("abort-pending", s1); + await settle("abort-later", s(2)); + try { + util.debounce(() => 1, 5, { signal: AbortSignal.abort("pre") }); + } catch (e) { + console.log("pre-aborted", e.name, e.code, e.message, e.cause); + } + + // Ordering against microtasks and a timer of the same delay. + const order = []; + const o = util.debounce(() => order.push("debounced"), 10); + o().then(() => order.push("then")); + setTimeout(() => order.push("timer"), 10); + Promise.resolve().then(() => order.push("micro")); + await new Promise((resolve) => setTimeout(resolve, 40)); + console.log(order); +} +main().then(tail); + +function tail() { + const tryIt = (run) => { try { run(); } catch (e) { console.log(e.name, e.code, e.message); } }; + tryIt(() => util.debounce(1, 5)); + tryIt(() => util.debounce(() => 1, -1)); + tryIt(() => util.debounce(() => 1, 1.5)); + tryIt(() => util.debounce(() => 1, "5")); + tryIt(() => util.debounce(() => 1, 5, null)); + tryIt(() => util.debounce(() => 1, 5, [1])); + tryIt(() => util.debounce(() => 1, 5, { leading: 1 })); + tryIt(() => util.debounce(() => 1, 5, { rejectOnCancel: "yes" })); + tryIt(() => util.debounce(() => 1, 5, { signal: {} })); + + const d = util.debounce(function named(a, b) { return a + b; }, 5); + console.log(typeof d, d.name, d.length, Object.keys(d), Object.getOwnPropertyNames(d)); + console.log(d); + const p = d(1, 2); + console.log(d.pending === p, d.pendingCount); + d.flush(); + + const holder = {}; + holder.run = util.debounce(function () { return this === holder.run; }, 5); + holder.run().then((same) => console.log("receiver is the function", same)); + let depth = 0; + const again = util.debounce(function (n) { + depth++; + if (n > 0) again(n - 1).then((v) => console.log("inner", v)); + return n; + }, 5); + again(2).then((v) => console.log("outer", v, depth)); + + const unrefd = util.debounce(() => console.log("never"), 500); + unrefd(); + unrefd.unref(); + process.on("exit", () => console.log("exit", unrefd.pendingCount)); +} diff --git a/tests/corpus/7711-util-throttle.cjs b/tests/corpus/7711-util-throttle.cjs new file mode 100644 index 000000000..f776e7174 --- /dev/null +++ b/tests/corpus/7711-util-throttle.cjs @@ -0,0 +1,101 @@ +// util.throttle: limits per interval, queueing, overflow 'drop', maxPending, +// concurrency, strict windows, cancel, options.signal, validation and the +// returned function's own members. +const util = require("node:util"); + +const settle = (label, p) => p.then((v) => console.log(label, "ok", v), (e) => console.log(label, "err", e.name, e.code, e.message)); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function main() { + // Two calls per 60 ms; the rest queue in order. + const seen = []; + const t = util.throttle(function (x) { seen.push(x); return x * 2; }, 2, 60); + const ps = [1, 2, 3, 4, 5].map((x) => t(x)); + console.log(seen, t.pendingCount, t.activeCount, t.pending === ps[4], t.hasImmediateCapacity()); + console.log(await Promise.all(ps), seen); + + // overflow 'drop' rejects with ERR_THROTTLED once the window is full. + const d = util.throttle((x) => x, 1, 1000, { overflow: "drop" }); + await settle("drop-first", d(1)); + await settle("drop-second", d(2)); + d.cancel(); + await settle("after-cancel", d(3)); + + // maxPending bounds the queue. + const m = util.throttle((x) => x, 1, 30, { maxPending: 1 }); + const m1 = m(1), m2 = m(2), m3 = m(3); + await settle("max1", m1); + await settle("max3", m3); + await settle("max2", m2); + + // concurrency counts unsettled async calls. + const order = []; + const c = util.throttle(async (x) => { order.push(`start ${x}`); await sleep(15); order.push(`end ${x}`); return x; }, 10, 1000, { concurrency: 1 }); + const ca = c("a"), cb = c("b"); + console.log(c.activeCount, c.pendingCount); + console.log(await ca, await cb, order, c.activeCount); + + // A queue that never empties while calls are taken and others arrive. + const q = util.throttle(async (x) => { await null; return x; }, 1000, 1000, { concurrency: 1 }); + const queued = []; + for (let i = 0; i < 5; i++) queued.push(q(i)); + const results = []; + for (let i = 5; i < 25; i++) { + results.push(await queued[i - 5]); + queued.push(q(i)); + } + for (let i = 20; i < 25; i++) results.push(await queued[i]); + console.log(results.join(","), q.pendingCount, q.activeCount); + + // A throwing fn rejects that call only. + const e = util.throttle((x) => { if (x === 2) throw new RangeError("two"); return x; }, 5, 10); + await settle("e1", e(1)); + await settle("e2", e(2)); + await settle("e3", e(3)); + + // strict keeps a rolling window. + const s = util.throttle((x) => x, 1, 25, { strict: true }); + const s1 = s(1), s2 = s(2); + console.log(s.hasImmediateCapacity(), await s1, await s2); + + // cancel rejects the queue and resets the limiter. + const k = util.throttle((x) => x, 1, 1000); + const k1 = k(1), k2 = k(2); + k.cancel("why"); + await settle("k1", k1); + await settle("k2", k2); + console.log(k.hasImmediateCapacity(), k.ref() === k, k.unref() === k); + await settle("k3", k(3)); + + // options.signal + const ac = new AbortController(); + const a = util.throttle((x) => x, 1, 1000, { signal: ac.signal }); + await settle("a1", a(1)); + const a2 = a(2); + ac.abort(); + await settle("a2", a2); + await settle("a3", a(3)); + console.log(a.hasImmediateCapacity(), a.pendingCount); +} +main().then(tail); + +function tail() { + const tryIt = (run) => { try { run(); } catch (e) { console.log(e.name, e.code, e.message); } }; + tryIt(() => util.throttle(null, 1, 5)); + tryIt(() => util.throttle(() => 1, 0, 5)); + tryIt(() => util.throttle(() => 1, 1.5, 5)); + tryIt(() => util.throttle(() => 1, 1, 2 ** 31)); + tryIt(() => util.throttle(() => 1, 1, 5, "options")); + tryIt(() => util.throttle(() => 1, 1, 5, { concurrency: 0 })); + tryIt(() => util.throttle(() => 1, 1, 5, { concurrency: "2" })); + tryIt(() => util.throttle(() => 1, 1, 5, { overflow: "x" })); + tryIt(() => util.throttle(() => 1, 1, 5, { maxPending: -1 })); + tryIt(() => util.throttle(() => 1, 1, 5, { strict: "y" })); + tryIt(() => util.throttle(() => 1, 1, 5, { signal: AbortSignal.abort("pre") })); + + const t = util.throttle(() => 1, 1, 50, { overflow: "drop" }); + console.log(Object.keys(t), Object.getOwnPropertyNames(t), t.name, t.length); + console.log(t); + t(); + t().catch((e) => console.log(e.name, e.code, e.message, Object.keys(e))); +} diff --git a/tests/corpus/7712-util-debounce-throttle.ts b/tests/corpus/7712-util-debounce-throttle.ts new file mode 100644 index 000000000..007635bac --- /dev/null +++ b/tests/corpus/7712-util-debounce-throttle.ts @@ -0,0 +1,27 @@ +// util.debounce and util.throttle through their TypeScript types. +import { debounce, throttle } from "node:util"; + +async function main(): Promise { + const save = debounce((text: string, times: number): string => text.repeat(times), 10); + const first = save("a", 1); + const last = save("b", 3); + console.log(save.pendingCount, save.pending !== null); + const results: string[] = await Promise.all([first, last]); + console.log(results, save.pendingCount); + + const doubled = throttle(async (n: number): Promise => n * 2, 2, 20); + const values: number[] = await Promise.all([doubled(1), doubled(2), doubled(3)]); + console.log(values, doubled.activeCount, doubled.hasImmediateCapacity()); + + const later = debounce((n: number) => n, 1000); + const pending = later(5); + later.cancel("stopped"); + try { + await pending; + } catch (e) { + const err = e as Error; + console.log(err.name, err.message, err.cause); + } + console.log(later.ref() === later, later.unref() === later); +} +main(); diff --git a/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts b/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts new file mode 100644 index 000000000..b7cb5418a --- /dev/null +++ b/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts @@ -0,0 +1,28 @@ +// U+11A7 (HANGUL JUNGSEONG O-YAE) is a vowel that composes with nothing: +// after an LV syllable, after a leading consonant, and around trailing +// consonants it stays as it is in every normalization form, as in Node. + +const samples = [ + "가ᆧ", + "가ᆧ", + "가ᆧᆨ", + "각ᆧ", + "ᄀᆧ", + "ᆧ", + "ᆧᆧ", + "x가ᆧy각z", + "café 가ᆧ é", + "ガᆧ가", +]; + +function points(text: string): string { + return Array.from(text, (ch) => ch.codePointAt(0)!.toString(16)).join("."); +} + +for (const sample of samples) { + console.log( + points(sample), + ["NFC", "NFD", "NFKC", "NFKD"].map((form) => points(sample.normalize(form))).join(" "), + ); +} +console.log("가ᆧ".normalize() === "가ᆧ", "가ᆧ".normalize().length); diff --git a/tests/corpus/8120-inspect-custom-nested.ts b/tests/corpus/8120-inspect-custom-nested.ts new file mode 100644 index 000000000..283f9e9cc --- /dev/null +++ b/tests/corpus/8120-inspect-custom-nested.ts @@ -0,0 +1,67 @@ +// util.inspect and util.format calls inside [util.inspect.custom] methods +// start from their own defaults: no indentation from the outer render, the +// outer circular references kept, and no colors from the outer options. +import * as util from "node:util"; + +class Wide { + v: number; + constructor(v: number) { + this.v = v; + } + [util.inspect.custom](): string { + return util.inspect({ a: { b: [1, 2, { c: this.v }] }, s: "x".repeat(50), t: "y".repeat(30) }); + } +} +console.log({ outer: { inner: new Wide(1) } }); +type Holder = { first: Wide; list: Wide[]; self?: Holder }; +const holder: Holder = { first: new Wide(2), list: [new Wide(3)] }; +holder.self = holder; +console.log(holder); +console.log(util.inspect({ deep: { deeper: { c: new Wide(4) } } })); + +const shared = { k: 1 }; +class Shares { + [util.inspect.custom](): string { + return "S<" + util.inspect(shared) + ">"; + } +} +console.log({ a: shared, c: new Shares(), b: shared }); +type Loop = { c: Shares; me?: Loop; again?: Loop }; +const loop: Loop = { c: new Shares() }; +loop.me = loop; +loop.again = loop; +console.log(loop); + +class Formats { + [util.inspect.custom](): string { + return util.format("%s|%O|%o|%j|%d", "a", "b", "c", "d", 5); + } +} +console.log(JSON.stringify(util.inspect(new Formats(), { colors: true }))); +console.log(JSON.stringify(util.inspect({ f: new Formats() }, { colors: true }))); + +class Leaf { + [util.inspect.custom](depth: unknown, options: any): string { + return options.stylize("leaf", "special") + util.inspect({ z: 1, a: [1, 2] }); + } +} +class Branch { + leaf = new Leaf(); + [util.inspect.custom](): string { + return "B(" + util.inspect({ b: 2, a: { leaf: this.leaf, s: "q" } }) + ")"; + } +} +class Throws { + [util.inspect.custom](): string { + throw new Error("nope"); + } +} +const opts = { sorted: true, breakLength: 20, compact: false, colors: true }; +console.log(JSON.stringify(util.inspect({ y: new Branch(), x: [new Leaf()] }, opts))); +console.log(util.inspect({ k: { j: new Branch() } }, { depth: 0 })); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("caught", (e as Error).message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8121-inspect-custom-nested.cjs b/tests/corpus/8121-inspect-custom-nested.cjs new file mode 100644 index 000000000..aa5c75ca5 --- /dev/null +++ b/tests/corpus/8121-inspect-custom-nested.cjs @@ -0,0 +1,69 @@ +// util.inspect and util.format calls inside [util.inspect.custom] methods +// start from their own defaults: no indentation from the outer render and +// no colors from the outer options. +const util = require("node:util"); + +class Wide { + constructor(v) { + this.v = v; + } + [util.inspect.custom]() { + return util.inspect({ a: { b: [1, 2, { c: this.v }] }, s: "x".repeat(50), t: "y".repeat(30) }); + } +} +console.log({ outer: { inner: new Wide(1) } }); +console.log({ first: new Wide(2), list: [new Wide(3)] }); +console.log(util.inspect({ deep: { deeper: { c: new Wide(4) } } })); + +const shared = { k: 1 }; +class Shares { + [util.inspect.custom]() { + return "S<" + util.inspect(shared) + ">"; + } +} +console.log({ a: shared, c: new Shares(), b: shared }); +class Formats { + [util.inspect.custom]() { + return util.format("%s|%O|%o|%j|%d", "a", "b", "c", "d", 5); + } +} +console.log(JSON.stringify(util.inspect(new Formats(), { colors: true }))); +console.log(JSON.stringify(util.inspect({ f: new Formats() }, { colors: true }))); +class FormatsObjects { + [util.inspect.custom]() { + return ( + util.format("%O %O", { s: "b" }, [1]) + + " " + + util.format("x", "y", 3) + + " " + + util.formatWithOptions({}, "%O", "z") + ); + } +} +console.log(JSON.stringify(util.inspect([new FormatsObjects()], { colors: true }))); + +class Leaf { + [util.inspect.custom](depth, options) { + return options.stylize("leaf", "special") + util.inspect({ z: 1, a: [1, 2] }); + } +} +class Branch { + leaf = new Leaf(); + [util.inspect.custom]() { + return "B(" + util.inspect({ b: 2, a: { leaf: this.leaf, s: "q" } }) + ")"; + } +} +class Throws { + [util.inspect.custom]() { + throw new Error("nope"); + } +} +const opts = { sorted: true, breakLength: 20, compact: false, colors: true }; +console.log(JSON.stringify(util.inspect({ y: new Branch(), x: [new Leaf()] }, opts))); +console.log(util.inspect({ k: { j: new Branch() } }, { depth: 0 })); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("caught", e.message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8122-inspect-custom-runtime-only.ts b/tests/corpus/8122-inspect-custom-runtime-only.ts new file mode 100644 index 000000000..d84dfa63d --- /dev/null +++ b/tests/corpus/8122-inspect-custom-runtime-only.ts @@ -0,0 +1,34 @@ +// Classes whose [util.inspect.custom] method runs only from the runtime +// inspect walk (instances held as unknown or in parsed JSON) compile with +// the functions and methods that method calls. +import * as util from "node:util"; +function helper(n: number): string { + return "H" + n; +} +class Base { + [util.inspect.custom](): string { + return helper(1) + this.tag(); + } + tag(): string { + return "-base"; + } +} +class Derived extends Base { + tag(): string { + return "-derived"; + } +} +class Box { + item: T; + constructor(item: T) { + this.item = item; + } + [util.inspect.custom](): string { + return "Box(" + helper(2) + ")"; + } +} +const xs: unknown[] = [new Derived(), new Box(3)]; +console.log(xs); +const j = JSON.parse('{"a":1}'); +j.d = new Derived(); +console.log(j); diff --git a/tests/corpus/8125-inspect-custom-throws.ts b/tests/corpus/8125-inspect-custom-throws.ts new file mode 100644 index 000000000..33077a32b --- /dev/null +++ b/tests/corpus/8125-inspect-custom-throws.ts @@ -0,0 +1,32 @@ +// A [util.inspect.custom] method that throws inside an outer inspect: an +// enclosing custom method that catches the error, and the walk around it, +// continue with their own indentation, circular references and colors. +import * as util from "node:util"; +class Throws { + [util.inspect.custom](): string { + throw new Error("inner"); + } +} +class Catches { + [util.inspect.custom](): string { + let note = ""; + try { + note = util.inspect({ a: { b: [new Throws()] } }); + } catch (e) { + note = "caught " + (e as Error).message; + } + return note + " then " + util.inspect({ x: { y: [1, 2] } }); + } +} +const shared = { k: 1 }; +type Outer = { first: { deep: Catches }; list: unknown[]; s: object; t: object; me?: Outer }; +const outer: Outer = { first: { deep: new Catches() }, list: [new Catches(), { z: 3 }], s: shared, t: shared }; +outer.me = outer; +console.log(util.inspect(outer, { breakLength: 50 })); +console.log(JSON.stringify(util.inspect([new Catches(), "s"], { colors: true }))); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("outer caught", (e as Error).message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/8126-inspect-custom-throws.cjs b/tests/corpus/8126-inspect-custom-throws.cjs new file mode 100644 index 000000000..8d4e97608 --- /dev/null +++ b/tests/corpus/8126-inspect-custom-throws.cjs @@ -0,0 +1,30 @@ +// A [util.inspect.custom] method that throws inside an outer inspect: an +// enclosing custom method that catches the error, and the walk around it, +// continue with their own indentation and colors. +const util = require("node:util"); +class Throws { + [util.inspect.custom]() { + throw new Error("inner"); + } +} +class Catches { + [util.inspect.custom]() { + let note = ""; + try { + note = util.inspect({ a: { b: [new Throws()] } }); + } catch (e) { + note = "caught " + e.message; + } + return note + " then " + util.inspect({ x: { y: [1, 2] } }); + } +} +const shared = { k: 1 }; +const outer = { first: { deep: new Catches() }, list: [new Catches(), { z: 3 }], s: shared, t: shared }; +console.log(util.inspect(outer, { breakLength: 50 })); +console.log(JSON.stringify(util.inspect([new Catches(), "s"], { colors: true }))); +try { + console.log(util.inspect({ a: { b: new Throws() } })); +} catch (e) { + console.log("outer caught", e.message); +} +console.log({ after: { deep: { x: 1 } }, list: [1, 2, 3] }); diff --git a/tests/corpus/dyn-union-function-arms.ts b/tests/corpus/dyn-union-function-arms.ts new file mode 100644 index 000000000..e85cc2e6b --- /dev/null +++ b/tests/corpus/dyn-union-function-arms.ts @@ -0,0 +1,48 @@ +// A union of a function type with a value the checked-dynamic tree holds +// (AbortSignal, `unknown`) rides that representation: the function is held +// by reference, `typeof x === "function"` narrows it back out, and calls, +// identity and the other arm's members behave as in Node. + +type Progress = (loaded: number, total: number) => void; + +function listAll( + dirOrProgress?: string | Progress, + progressOrSignal?: Progress | AbortSignal, + signal?: AbortSignal, +): string { + const dir = typeof dirOrProgress === "string" ? dirOrProgress : "/default"; + const progress = + typeof dirOrProgress === "function" + ? dirOrProgress + : typeof progressOrSignal === "function" + ? progressOrSignal + : undefined; + const abortSignal = + typeof dirOrProgress === "string" || typeof progressOrSignal === "function" + ? signal + : (progressOrSignal ?? signal); + progress?.(1, 3); + progress?.(3, 3); + return `${dir} aborted=${abortSignal?.aborted ?? "none"}`; +} + +const seen: string[] = []; +const report: Progress = (loaded, total) => seen.push(`${loaded}/${total}`); +const controller = new AbortController(); +console.log(listAll()); +console.log(listAll("/x", report)); +console.log(listAll(report, controller.signal)); +controller.abort(); +console.log(listAll("/y", undefined, controller.signal)); +console.log(listAll(report, controller.signal), seen.join(" ")); + +function same(handler: Progress | AbortSignal | undefined): boolean { + return handler === report; +} +console.log(same(report), same(controller.signal), same(undefined), same((a, b) => void (a + b))); + +function describe(value: unknown | ((n: number) => string)): string { + if (typeof value === "function") return `function -> ${(value as (n: number) => string)(7)}`; + return `value ${JSON.stringify(value)}`; +} +console.log(describe((n: number) => `n=${n}`), describe({ a: 1 }), describe("s")); diff --git a/tests/corpus/generic-member-lambda-impls.ts b/tests/corpus/generic-member-lambda-impls.ts new file mode 100644 index 000000000..fd25bae96 --- /dev/null +++ b/tests/corpus/generic-member-lambda-impls.ts @@ -0,0 +1,40 @@ +// An object literal implements an interface's generic method with a plain +// lambda (`send: (m) => …` for `send(m: Message): string`). The +// lambda takes the contextual signature's type parameters as its own and +// lowers once, erased, like a generic lambda: its parameters hold whatever +// each call passes. + +interface Message { + content: string; + details?: T; +} + +interface Sender { + name: string; + send(message: Message): string; + notify(message: Message): void; + pick(keys: K[], fallback: K): K; +} + +const log: string[] = []; + +function makeSender(name: string): Sender { + return { + name, + send: (message) => `${name} sent ${message.content} (${message.details === undefined ? "no details" : JSON.stringify(message.details)})`, + notify: (message) => { + log.push(`${name} notified ${message.content}`); + }, + pick: (keys, fallback) => keys[0] ?? fallback, + }; +} + +const senders: Sender[] = [makeSender("first"), makeSender("second")]; +for (const sender of senders) { + console.log(sender.send({ content: "hi", details: { n: 3 } })); + console.log(sender.send({ content: "bare" })); + console.log(sender.send({ content: "list", details: [1, 2] })); + sender.notify({ content: "ping", details: true }); + console.log(sender.pick(["a", "b"], "z"), sender.pick([], "z")); +} +console.log(log.join("; ")); diff --git a/tests/corpus/native-worker-terminate-bounded-loops.ts b/tests/corpus/native-worker-terminate-bounded-loops.ts new file mode 100644 index 000000000..ec1e4c736 --- /dev/null +++ b/tests/corpus/native-worker-terminate-bounded-loops.ts @@ -0,0 +1,102 @@ +// worker.terminate() stops a worker whose loop runs short inner loops: +// counted loops of a few iterations, a two-counter loop, an inner loop +// whose length grows past 64 iterations, an inner loop too long to finish, +// a nest of loops with constant bounds, and an inner loop whose counter +// stops changing at 2 ** 53 (`j + 1 === j` there), so it never ends. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +function counted(values: number[]): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < values.length; i++) acc = (acc + values[i]! * i) % 1000003; + for (let i = values.length - 1; i >= 0; i -= 2) acc = (acc * 7 + values[i]!) % 1000003; + if (round === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function paired(values: number[]): number { + let flips = 0; + for (let round = 0; ; round++) { + for (let i = 0, j = values.length - 1; i < j; i++, j--) { + const t = values[i]!; + values[i] = values[j]!; + values[j] = t; + flips++; + } + if (round === 200) parentPort!.postMessage("ready"); + } + return flips; +} + +function growing(): number { + let acc = 0; + for (let n = 1; ; n++) { + const limit = n % 200; + for (let i = 0; i < limit; i++) acc = (acc + i) % 1000003; + if (n === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function long(limit: number): number { + let acc = 0; + for (let round = 0; ; round++) { + for (let i = 0; i < limit; i++) { + acc = (acc * 7 + i) % 1000003; + if (i === 100000) parentPort!.postMessage("ready"); + } + } + return acc; +} + +const ROUNDS = 50; + +function constant(row: Float64Array): number { + let acc = 0; + for (let y = 0; ; y++) { + for (let i = 0; i < 8; i += 2) { + const c = row[i]!; + for (let j = 0; j < ROUNDS; j++) { + acc = (acc * 3 + j + c) % 1000003; + if (acc === -1) break; + } + } + if (y === 200) parentPort!.postMessage("ready"); + } + return acc; +} + +function stalled(): number { + let acc = 0; + for (let round = 0; ; round++) { + if (round === 0) parentPort!.postMessage("ready"); + for (let j = 9007199254740990; j < 9007199254741002; j++) acc = (acc + (j % 7)) % 1000003; + } + return acc; +} + +if (isMainThread) { + for (const kind of ["counted", "paired", "growing", "long", "constant", "stalled"]) { + await new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { workerData: kind }); + worker.once("message", () => { + void worker.terminate().then((code: number) => console.log(kind, "terminate", code)); + }); + worker.once("exit", (code: number) => { + console.log(kind, "exit", code); + resolve(); + }); + }); + } +} else { + const values = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8]; + let result = 0; + if (workerData === "counted") result = counted(values); + else if (workerData === "paired") result = paired(values); + else if (workerData === "growing") result = growing(); + else if (workerData === "long") result = long(1e15); + else if (workerData === "stalled") result = stalled(); + else result = constant(new Float64Array(8).fill(0.25)); + console.log("unexpected result", result); +} diff --git a/tests/corpus/recursive-optional-callback-params.ts b/tests/corpus/recursive-optional-callback-params.ts new file mode 100644 index 000000000..0c81235d6 --- /dev/null +++ b/tests/corpus/recursive-optional-callback-params.ts @@ -0,0 +1,107 @@ +// An optional parameter whose type reaches back to the interface that +// declares the method keeps its whole union. A command context's +// `newSession(options?)` takes callbacks typed by a subinterface of the +// context, so mapping the context passes through the options union while +// that union is still being mapped; the parameter still holds the options +// record, not only `undefined`. + +interface CommandContext { + cwd: string; + newSession(options?: { + parent?: string; + withSession?: (ctx: ReplacedContext) => Promise; + }): Promise<{ cancelled: boolean }>; + fork( + entryId: string, + options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedContext) => Promise }, + ): Promise<{ cancelled: boolean }>; +} + +interface ReplacedContext extends CommandContext { + depth: number; + send(content: string): Promise; +} + +interface Command { + description?: string; + handler: (args: string, ctx: CommandContext) => Promise; +} + +const log: string[] = []; +const commands = new Map(); + +function register(name: string, command: Command): void { + commands.set(name, command); +} + +register("new", { + description: "start a session", + handler: async (args, ctx) => { + const first = await ctx.newSession({ + parent: args, + withSession: async (next) => { + await next.send(`hello from ${next.cwd} at depth ${next.depth}`); + const nested = await next.newSession({ + withSession: async (inner) => { + await inner.send(`nested at depth ${inner.depth}`); + }, + }); + log.push(`nested cancelled=${nested.cancelled}`); + }, + }); + log.push(`first cancelled=${first.cancelled}`); + const bare = await ctx.newSession(); + log.push(`bare cancelled=${bare.cancelled}`); + }, +}); + +register("fork", { + handler: async (args, ctx) => { + const r = await ctx.fork(args, { + position: "at", + withSession: async (next) => next.send(`forked ${args} at depth ${next.depth}`), + }); + log.push(`fork cancelled=${r.cancelled}`); + const plain = await ctx.fork(args); + log.push(`plain fork cancelled=${plain.cancelled}`); + }, +}); + +function makeContext(depth: number): ReplacedContext { + return { + cwd: `/work/${depth}`, + depth, + newSession: async (options) => { + log.push(`newSession depth=${depth} parent=${options?.parent ?? "-"}`); + if (options?.withSession) { + await options.withSession(makeContext(depth + 1)); + return { cancelled: false }; + } + return { cancelled: true }; + }, + fork: async (entryId, options) => { + log.push(`fork ${entryId} position=${options?.position ?? "-"}`); + const callback = options?.withSession; + if (callback === undefined) return { cancelled: true }; + await callback(makeContext(depth + 1)); + return { cancelled: false }; + }, + send: async (content) => { + log.push(`send: ${content}`); + }, + }; +} + +async function run(): Promise { + const root: CommandContext = makeContext(0); + for (const [name, command] of commands) { + log.push(`run ${name} (${command.description ?? "no description"})`); + await command.handler(`entry-${name}`, root); + } + for (const line of log) console.log(line); +} + +run().catch((e: unknown) => { + console.log("failed", e instanceof Error ? e.message : String(e)); + process.exitCode = 1; +}); diff --git a/tests/diagnostics/inspect-custom-fences.ts b/tests/diagnostics/inspect-custom-fences.ts new file mode 100644 index 000000000..404f6ddab --- /dev/null +++ b/tests/diagnostics/inspect-custom-fences.ts @@ -0,0 +1,37 @@ +// [util.inspect.custom] methods keep their boundaries: a class whose +// subclass declares the method without declaring it itself, a method with +// more than three parameters, a method returning a class instance, and %o +// over instances with one. +import { format, inspect } from "node:util"; + +class Plain { + a = 1; +} +class Fancy extends Plain { + [inspect.custom](): string { + return "fancy"; + } +} +const items: Plain[] = [new Fancy()]; +console.log(inspect(items)); + +class Wide { + [inspect.custom](a: number, b: unknown, c: unknown, d: unknown): string { + return `${a}${String(b)}${String(c)}${String(d)}`; + } +} +console.log(inspect(new Wide())); + +class Echo { + [inspect.custom](): Echo { + return new Echo(); + } +} +console.log(inspect(new Echo())); + +class Shown { + [inspect.custom](): string { + return "shown"; + } +} +console.log(format("%o", new Shown())); diff --git a/tests/harness/__snapshots__/inspect-custom-fences.ts.txt b/tests/harness/__snapshots__/inspect-custom-fences.ts.txt new file mode 100644 index 000000000..4f5da9030 --- /dev/null +++ b/tests/harness/__snapshots__/inspect-custom-fences.ts.txt @@ -0,0 +1,35 @@ +inspect-custom-fences.ts:16:21 - error SC2020: 'util.inspect of 'Plain[]' values' is part of the standard library types but has no scriptc lowering yet + + 15 | const items: Plain[] = [new Fancy()]; + 16 | console.log(inspect(items)); + | ^~~~~ + 17 | + + hint: classes whose subclasses declare [util.inspect.custom] have no inspect lowering yet (declare the method on the class itself) + +inspect-custom-fences.ts:23:21 - error SC2020: 'util.inspect of 'Wide' values' is part of the standard library types but has no scriptc lowering yet + + 22 | } + 23 | console.log(inspect(new Wide())); + | ^~~~~~~~~~ + 24 | + + hint: [util.inspect.custom] methods with more than three parameters have no inspect lowering + +inspect-custom-fences.ts:30:21 - error SC2020: 'util.inspect of 'Echo' values' is part of the standard library types but has no scriptc lowering yet + + 29 | } + 30 | console.log(inspect(new Echo())); + | ^~~~~~~~~~ + 31 | + + hint: [util.inspect.custom] methods that return a class instance have no inspect lowering + +inspect-custom-fences.ts:37:26 - error SC2020: 'util.format %o over values with [util.inspect.custom] methods' is part of the standard library types but has no scriptc lowering yet + + 36 | } + 37 | console.log(format("%o", new Shown())); + | ^~~~~~~~~~~ + 38 | + + hint: %o passes showHidden to the methods — use %O \ No newline at end of file diff --git a/tests/harness/corpus-costs/7520-util-format-runtime.ts.json b/tests/harness/corpus-costs/7520-util-format-runtime.ts.json new file mode 100644 index 000000000..9cb750202 --- /dev/null +++ b/tests/harness/corpus-costs/7520-util-format-runtime.ts.json @@ -0,0 +1 @@ +{ "plain": 6414, "san": 9821 } diff --git a/tests/harness/corpus-costs/7521-util-format-runtime.cjs.json b/tests/harness/corpus-costs/7521-util-format-runtime.cjs.json new file mode 100644 index 000000000..05cc5696d --- /dev/null +++ b/tests/harness/corpus-costs/7521-util-format-runtime.cjs.json @@ -0,0 +1 @@ +{ "plain": 4635, "san": 6305 } diff --git a/tests/harness/corpus-costs/7522-promisify-untyped.cjs.json b/tests/harness/corpus-costs/7522-promisify-untyped.cjs.json new file mode 100644 index 000000000..c5358642b --- /dev/null +++ b/tests/harness/corpus-costs/7522-promisify-untyped.cjs.json @@ -0,0 +1 @@ +{ "plain": 3984, "san": 6122 } diff --git a/tests/harness/corpus-costs/7523-promisify-untyped.ts.json b/tests/harness/corpus-costs/7523-promisify-untyped.ts.json new file mode 100644 index 000000000..be46b70d4 --- /dev/null +++ b/tests/harness/corpus-costs/7523-promisify-untyped.ts.json @@ -0,0 +1 @@ +{ "plain": 3595, "san": 36166 } diff --git a/tests/harness/corpus-costs/7524-inspect-custom.ts.json b/tests/harness/corpus-costs/7524-inspect-custom.ts.json new file mode 100644 index 000000000..b9c089238 --- /dev/null +++ b/tests/harness/corpus-costs/7524-inspect-custom.ts.json @@ -0,0 +1 @@ +{ "plain": 5331, "san": 10004 } diff --git a/tests/harness/corpus-costs/7525-inspect-custom.cjs.json b/tests/harness/corpus-costs/7525-inspect-custom.cjs.json new file mode 100644 index 000000000..010f8b482 --- /dev/null +++ b/tests/harness/corpus-costs/7525-inspect-custom.cjs.json @@ -0,0 +1 @@ +{ "plain": 4178, "san": 6619 } diff --git a/tests/harness/corpus-costs/7526-inspect-options-runtime.ts.json b/tests/harness/corpus-costs/7526-inspect-options-runtime.ts.json new file mode 100644 index 000000000..0599901b9 --- /dev/null +++ b/tests/harness/corpus-costs/7526-inspect-options-runtime.ts.json @@ -0,0 +1 @@ +{ "plain": 4855, "san": 7570 } diff --git a/tests/harness/corpus-costs/7536-inspect-custom-errors.ts.json b/tests/harness/corpus-costs/7536-inspect-custom-errors.ts.json new file mode 100644 index 000000000..bc864c5b3 --- /dev/null +++ b/tests/harness/corpus-costs/7536-inspect-custom-errors.ts.json @@ -0,0 +1 @@ +{ "plain": 5921, "san": 72405 } diff --git a/tests/harness/corpus-costs/7537-inspect-custom-errors.cjs.json b/tests/harness/corpus-costs/7537-inspect-custom-errors.cjs.json new file mode 100644 index 000000000..cfee42705 --- /dev/null +++ b/tests/harness/corpus-costs/7537-inspect-custom-errors.cjs.json @@ -0,0 +1 @@ +{ "plain": 4421, "san": 8837 } diff --git a/tests/harness/corpus-costs/7538-inspect-custom-prototypes.mjs.json b/tests/harness/corpus-costs/7538-inspect-custom-prototypes.mjs.json new file mode 100644 index 000000000..d5314d959 --- /dev/null +++ b/tests/harness/corpus-costs/7538-inspect-custom-prototypes.mjs.json @@ -0,0 +1 @@ +{ "plain": 2529, "san": 2333 } diff --git a/tests/harness/corpus-costs/7700-util-deprecated-helpers.cjs.json b/tests/harness/corpus-costs/7700-util-deprecated-helpers.cjs.json new file mode 100644 index 000000000..9e59ed2d0 --- /dev/null +++ b/tests/harness/corpus-costs/7700-util-deprecated-helpers.cjs.json @@ -0,0 +1 @@ +{ "plain": 6131, "san": 10266 } diff --git a/tests/harness/corpus-costs/7701-util-deprecated-helpers.ts.json b/tests/harness/corpus-costs/7701-util-deprecated-helpers.ts.json new file mode 100644 index 000000000..d9de27c92 --- /dev/null +++ b/tests/harness/corpus-costs/7701-util-deprecated-helpers.ts.json @@ -0,0 +1 @@ +{ "plain": 5541, "san": 12389 } diff --git a/tests/harness/corpus-costs/7702-util-signal-exit-code.cjs.json b/tests/harness/corpus-costs/7702-util-signal-exit-code.cjs.json new file mode 100644 index 000000000..93d4cfd1b --- /dev/null +++ b/tests/harness/corpus-costs/7702-util-signal-exit-code.cjs.json @@ -0,0 +1 @@ +{ "plain": 5273, "san": 8954 } diff --git a/tests/harness/corpus-costs/7703-util-aborted.cjs.json b/tests/harness/corpus-costs/7703-util-aborted.cjs.json new file mode 100644 index 000000000..9a0b7ccf5 --- /dev/null +++ b/tests/harness/corpus-costs/7703-util-aborted.cjs.json @@ -0,0 +1 @@ +{ "plain": 5462, "san": 10299 } diff --git a/tests/harness/corpus-costs/7704-util-aborted.ts.json b/tests/harness/corpus-costs/7704-util-aborted.ts.json new file mode 100644 index 000000000..24879623e --- /dev/null +++ b/tests/harness/corpus-costs/7704-util-aborted.ts.json @@ -0,0 +1 @@ +{ "plain": 6045, "san": 10336 } diff --git a/tests/harness/corpus-costs/7705-util-diff.cjs.json b/tests/harness/corpus-costs/7705-util-diff.cjs.json new file mode 100644 index 000000000..cfa6c89a3 --- /dev/null +++ b/tests/harness/corpus-costs/7705-util-diff.cjs.json @@ -0,0 +1 @@ +{ "plain": 5530, "san": 9522 } diff --git a/tests/harness/corpus-costs/7706-util-diff.ts.json b/tests/harness/corpus-costs/7706-util-diff.ts.json new file mode 100644 index 000000000..2f2a5b423 --- /dev/null +++ b/tests/harness/corpus-costs/7706-util-diff.ts.json @@ -0,0 +1 @@ +{ "plain": 5532, "san": 9167 } diff --git a/tests/harness/corpus-costs/7707-util-mime-type.cjs.json b/tests/harness/corpus-costs/7707-util-mime-type.cjs.json new file mode 100644 index 000000000..69d56fb64 --- /dev/null +++ b/tests/harness/corpus-costs/7707-util-mime-type.cjs.json @@ -0,0 +1 @@ +{ "plain": 7466, "san": 14712 } diff --git a/tests/harness/corpus-costs/7708-util-mime-type.ts.json b/tests/harness/corpus-costs/7708-util-mime-type.ts.json new file mode 100644 index 000000000..5400687cd --- /dev/null +++ b/tests/harness/corpus-costs/7708-util-mime-type.ts.json @@ -0,0 +1 @@ +{ "plain": 5938, "san": 13241 } diff --git a/tests/harness/corpus-costs/7709-function-own-properties.cjs.json b/tests/harness/corpus-costs/7709-function-own-properties.cjs.json new file mode 100644 index 000000000..615caa295 --- /dev/null +++ b/tests/harness/corpus-costs/7709-function-own-properties.cjs.json @@ -0,0 +1 @@ +{ "plain": 5394, "san": 9208 } diff --git a/tests/harness/corpus-costs/7710-util-debounce.cjs.json b/tests/harness/corpus-costs/7710-util-debounce.cjs.json new file mode 100644 index 000000000..3a5f6ba71 --- /dev/null +++ b/tests/harness/corpus-costs/7710-util-debounce.cjs.json @@ -0,0 +1 @@ +{ "plain": 7005, "san": 20190 } diff --git a/tests/harness/corpus-costs/7711-util-throttle.cjs.json b/tests/harness/corpus-costs/7711-util-throttle.cjs.json new file mode 100644 index 000000000..3b4d2438e --- /dev/null +++ b/tests/harness/corpus-costs/7711-util-throttle.cjs.json @@ -0,0 +1 @@ +{ "plain": 8250, "san": 15025 } diff --git a/tests/harness/corpus-costs/7712-util-debounce-throttle.ts.json b/tests/harness/corpus-costs/7712-util-debounce-throttle.ts.json new file mode 100644 index 000000000..bb4ea7997 --- /dev/null +++ b/tests/harness/corpus-costs/7712-util-debounce-throttle.ts.json @@ -0,0 +1 @@ +{ "plain": 5966, "san": 11757 } diff --git a/tests/harness/corpus-costs/8009-normalize-hangul-jungseong-o-yae.ts.json b/tests/harness/corpus-costs/8009-normalize-hangul-jungseong-o-yae.ts.json new file mode 100644 index 000000000..6c20e6ad7 --- /dev/null +++ b/tests/harness/corpus-costs/8009-normalize-hangul-jungseong-o-yae.ts.json @@ -0,0 +1 @@ +{ "plain": 2503, "san": 2110 } diff --git a/tests/harness/corpus-costs/8120-inspect-custom-nested.ts.json b/tests/harness/corpus-costs/8120-inspect-custom-nested.ts.json new file mode 100644 index 000000000..745c35791 --- /dev/null +++ b/tests/harness/corpus-costs/8120-inspect-custom-nested.ts.json @@ -0,0 +1 @@ +{ "plain": 5488, "san": 58791 } diff --git a/tests/harness/corpus-costs/8121-inspect-custom-nested.cjs.json b/tests/harness/corpus-costs/8121-inspect-custom-nested.cjs.json new file mode 100644 index 000000000..baf450a0d --- /dev/null +++ b/tests/harness/corpus-costs/8121-inspect-custom-nested.cjs.json @@ -0,0 +1 @@ +{ "plain": 4226, "san": 8699 } diff --git a/tests/harness/corpus-costs/8122-inspect-custom-runtime-only.ts.json b/tests/harness/corpus-costs/8122-inspect-custom-runtime-only.ts.json new file mode 100644 index 000000000..547cd3ddc --- /dev/null +++ b/tests/harness/corpus-costs/8122-inspect-custom-runtime-only.ts.json @@ -0,0 +1 @@ +{ "plain": 2738, "san": 4052 } diff --git a/tests/harness/corpus-costs/8125-inspect-custom-throws.ts.json b/tests/harness/corpus-costs/8125-inspect-custom-throws.ts.json new file mode 100644 index 000000000..7d8f08f56 --- /dev/null +++ b/tests/harness/corpus-costs/8125-inspect-custom-throws.ts.json @@ -0,0 +1 @@ +{ "plain": 3090, "san": 5295 } diff --git a/tests/harness/corpus-costs/8126-inspect-custom-throws.cjs.json b/tests/harness/corpus-costs/8126-inspect-custom-throws.cjs.json new file mode 100644 index 000000000..49ea4c6d4 --- /dev/null +++ b/tests/harness/corpus-costs/8126-inspect-custom-throws.cjs.json @@ -0,0 +1 @@ +{ "plain": 2826, "san": 4308 } diff --git a/tests/harness/corpus-costs/dyn-union-function-arms.ts.json b/tests/harness/corpus-costs/dyn-union-function-arms.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/dyn-union-function-arms.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/generic-member-lambda-impls.ts.json b/tests/harness/corpus-costs/generic-member-lambda-impls.ts.json new file mode 100644 index 000000000..1254aa6e4 --- /dev/null +++ b/tests/harness/corpus-costs/generic-member-lambda-impls.ts.json @@ -0,0 +1 @@ +{ "plain": 4000 } diff --git a/tests/harness/corpus-costs/native-worker-terminate-bounded-loops.ts.json b/tests/harness/corpus-costs/native-worker-terminate-bounded-loops.ts.json new file mode 100644 index 000000000..bb744e4ed --- /dev/null +++ b/tests/harness/corpus-costs/native-worker-terminate-bounded-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 4034, "san": 5956 } diff --git a/tests/harness/corpus-costs/recursive-optional-callback-params.ts.json b/tests/harness/corpus-costs/recursive-optional-callback-params.ts.json new file mode 100644 index 000000000..c33a4b32c --- /dev/null +++ b/tests/harness/corpus-costs/recursive-optional-callback-params.ts.json @@ -0,0 +1 @@ +{ "plain": 6000 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 3e59e534f..0a66038ac 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -2283,6 +2283,53 @@ console.log("unreachable"); }); }); +// The runtime formatter (format strings known only at run time, spread +// arguments, options objects) throws a coded SC2020 error for renderings +// it has no lowering for. +describe("runtime format refusals", () => { + test("%s of a class instance with toString, %o of objects, colored errors and options", async () => { + const r = await compileAndRun( + "format-runtime-refusals", + `import * as util from "node:util"; +class Named { + name = "n"; + toString(): string { + return this.name; + } +} +const s: string = process.argv.length > 99 ? "" : "%s"; +const o: string = process.argv.length > 99 ? "" : "%o"; +const attempts: Array<() => string> = [ + () => util.format(s, new Named()), + () => util.format(o, { a: 1 }), + () => util.formatWithOptions({ colors: true }, s, "x", new Error("e")), + () => util.formatWithOptions({ maxArrayLength: 1 }, [1, 2]), + () => util.formatWithOptions({ compact: true }, [1, 2]), +]; +for (const attempt of attempts) { + try { + console.log(attempt()); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(e.name, e.code, e.message); + } +} +console.log(util.format(s, { a: 1 }), util.formatWithOptions({ maxArrayLength: 100 }, [1, 2])); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "Error SC2020 util.format %s of a class instance whose class defines toString has no lowering\n" + + "Error SC2020 util.format %o of objects renders hidden properties, which have no lowering; use %O\n" + + "Error SC2020 colored inspect output of Error values has no lowering\n" + + "Error SC2020 util.formatWithOptions with the maxArrayLength option has no lowering\n" + + "Error SC2020 util.formatWithOptions with this compact option has no lowering (compact must be false or a number)\n" + + "{ a: 1 } [ 1, 2 ]\n", + ); + expect(r.stderr).toBe(""); + }); +}); + describe("numeric properties of native arrays", () => { // A native array keeps its numeric properties ("-1", "1.5") in a property // table that holds values without attributes, so defining one with a diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index d51a2e242..8e133f6e2 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -74,7 +74,7 @@ const KNOWN: Record = { "node24-file-url-conversion": { build: "SC1090" }, "node24-fs-descriptors": { run: "SC2020" }, "node24-mime": { run: "SC2020" }, - "node24-mime-errors": { run: "SC1090" }, + "node24-mime-errors": { run: "SC2020" }, "node24-no-error-assertions": { run: "SC2020" }, "node24-parseargs": { run: "SC1090" }, "node24-promise-timers": { run: "SC2020" },