Merge main

This commit is contained in:
Chris Tate
2026-10-25 18:30:23 -05:00
169 changed files with 8015 additions and 494 deletions
+2
View File
@@ -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/<name>/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/<program>.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.
@@ -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 }`.
@@ -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.
+72 -72
View File
@@ -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"
}
},
{
@@ -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"
},
+16 -3
View File
@@ -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"] },
+131 -5
View File
@@ -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<string>;
set(name: string, value: string): void;
values(): IterableIterator<string>;
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<T extends (...args: any[]) => any> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
cancel(reason?: unknown): void;
flush(): void;
ref(): this;
unref(): this;
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
readonly pendingCount: number;
}
export function debounce<T extends (...args: any[]) => any>(
fn: T,
wait: number,
options?: DebounceOptions,
): DebouncedFunction<T>;
export interface ThrottleOptions {
concurrency?: number;
maxPending?: number;
overflow?: "queue" | "drop";
signal?: AbortSignal;
strict?: boolean;
}
export interface ThrottledFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
cancel(reason?: unknown): void;
hasImmediateCapacity(): boolean;
ref(): this;
unref(): this;
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
readonly pendingCount: number;
readonly activeCount: number;
}
export function throttle<T extends (...args: any[]) => any>(
fn: T,
limit: number,
interval: number,
options?: ThrottleOptions,
): ThrottledFunction<T>;
/** Resolves with the abort event once `signal` aborts, or with undefined
* when it already has. */
export function aborted(signal: AbortSignal, resource: any): Promise<unknown>;
/** 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<T extends object>(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;
@@ -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,
@@ -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<IrStmt, { kind: "for" }>,
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;
@@ -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);
@@ -86,6 +86,17 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"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<string>([
"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",
+2 -2
View File
@@ -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;
@@ -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;
@@ -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;
@@ -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
@@ -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,
@@ -76,6 +76,8 @@ export interface EarlyExecutableNativeFeatures {
declAdopt?: boolean;
dynForAwait?: boolean;
eventsOnceSignal?: boolean;
mime?: boolean;
utilTiming?: boolean;
dynBuffer?: boolean;
builtinValues?: boolean;
events: boolean;
@@ -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<typeof locOf>,
): 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<MimeClass, ReadonlySet<string>> = {
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"
);
}
@@ -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 });
}
}
@@ -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,
};
@@ -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();
@@ -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);
@@ -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
@@ -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)
) {
@@ -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;
@@ -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.
@@ -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;
}
@@ -284,6 +284,28 @@ import { lowerRecordInheritedCall } from "./record-inherited.js";
const ZLIB_STREAM_METHODS: ReadonlySet<string> = 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.
@@ -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 } : {}),
@@ -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);
}
@@ -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<T>(m: Msg<T>): 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)
File diff suppressed because it is too large Load Diff
@@ -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
@@ -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")
@@ -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",
@@ -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 (
+72 -5
View File
@@ -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<T>` with T bound to `unknown`: the checked-dynamic value
+2
View File
@@ -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,
@@ -185,6 +185,16 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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<IrLibFn> = 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",
+54 -1
View File
@@ -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 },
+13
View File
@@ -575,6 +575,19 @@ const DYN_READ_ONLY = new Set<string>([
"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
+22
View File
@@ -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<string, IrLocal>,
): { 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
+25 -1
View File
@@ -103,6 +103,11 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = 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<IrLibFn> = 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<IrLibFn> = 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,
@@ -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,
+42
View File
@@ -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",
@@ -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 });
}
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7520-util-format-runtime.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7521-util-format-runtime.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7522-promisify-untyped.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7523-promisify-untyped.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7524-inspect-custom.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7525-inspect-custom.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7526-inspect-options-runtime.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7536-inspect-custom-errors.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7537-inspect-custom-errors.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7538-inspect-custom-prototypes.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7700-util-deprecated-helpers.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7701-util-deprecated-helpers.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7702-util-signal-exit-code.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7703-util-aborted.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7704-util-aborted.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7705-util-diff.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7706-util-diff.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7707-util-mime-type.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7708-util-mime-type.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7709-function-own-properties.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7710-util-debounce.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7711-util-throttle.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7712-util-debounce-throttle.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8009-normalize-hangul-jungseong-o-yae.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8120-inspect-custom-nested.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8121-inspect-custom-nested.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8122-inspect-custom-runtime-only.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8125-inspect-custom-throws.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8126-inspect-custom-throws.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dyn-union-function-arms.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/generic-member-lambda-impls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/native-worker-terminate-bounded-loops.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/recursive-optional-callback-params.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/inspect-custom-fences.ts"
],
"diags": []
}
@@ -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")],
+5 -1
View File
@@ -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 ───────────────────────────────────────────────────
+6
View File
@@ -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);
+57
View File
@@ -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;
}
+2
View File
@@ -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,
File diff suppressed because it is too large Load Diff
+82 -1
View File
@@ -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);
+25 -3
View File
@@ -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);
+56 -2
View File
@@ -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) {
+603
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
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);
}
+30
View File
@@ -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;
}
+3
View File
@@ -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). */
+18
View File
@@ -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);
+53 -2
View File
@@ -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;
+5
View File
@@ -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;
}
+221 -160
View File
@@ -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 <stdio.h>
@@ -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;
}
File diff suppressed because it is too large Load Diff
+20 -4
View File
@@ -16,13 +16,16 @@
* [--seeds 1,2,3] [--jobs 8] [--entry <file.ts>]... [--out <dir>]
* [--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 <out>/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)),
+22 -7
View File
@@ -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"))
+110
View File
@@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More