From 3008f2ff1a6b2715d0150a765191a42d4ee6de98 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 14 Oct 2026 08:08:06 -0500 Subject: [PATCH] Support util.inherits, util.deprecate, util.callbackify and util.debuglog --- docs/content/docs/limitations.mdx | 4 +- .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 74 +- .../generated/node-v24-backlog.json | 180 ++-- .../generated/node-v24-internal.json | 124 ++- internal/compatibility/src/generate.mjs | 24 + internal/compatibility/static-support.json | 9 + .../ambient/scriptc-node-fallback.d.ts | 58 ++ .../compiler/src/backend/llvm/lib-shared.ts | 11 + .../compiler/src/backend/native/executable.ts | 3 + .../src/frontend/lowering/builtins/process.ts | 18 +- .../src/frontend/lowering/lower-calls.ts | 8 +- .../src/frontend/lowering/lower-inspect.ts | 87 ++ .../src/frontend/lowering/lower-stmts.ts | 18 + .../src/frontend/lowering/surfaces.ts | 47 + packages/compiler/src/frontend/type-mapper.ts | 6 +- packages/compiler/src/ir/builtin-effects.ts | 6 + .../compiler/src/ir/builtin-signatures.ts | 22 +- packages/compiler/src/ir/runtime-features.ts | 21 + packages/compiler/surface-manifest.json | 30 + packages/compiler/test/cc-driver.test.ts | 5 + .../tests/corpus/5185-util-inherits.cjs.json | 6 + .../tests/corpus/5186-util-inherits.ts.json | 6 + .../tests/corpus/5187-util-deprecate.cjs.json | 6 + .../corpus/5188-util-deprecate-flags.cjs.json | 6 + .../tests/corpus/5189-util-deprecate.ts.json | 6 + .../corpus/5190-util-callbackify.cjs.json | 6 + .../corpus/5191-util-callbackify.ts.json | 6 + .../tests/corpus/5192-util-debuglog.cjs.json | 6 + .../tests/corpus/5193-util-debuglog.ts.json | 6 + .../5194-process-emit-warning-tick.cjs.json | 6 + .../node-types/util-callables.ts.json | 6 + .../runtime-pack-matrix.mjs | 1 + .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_async_dyn.c | 142 ++- packages/runtime/src/scr_json.c | 11 +- packages/runtime/src/scr_lib.c | 32 + packages/runtime/src/scr_runtime.h | 36 +- packages/runtime/src/scr_util_callable.c | 894 ++++++++++++++++++ packages/runtime/src/scr_util_style.c | 6 +- .../corpus/2215-process-emit-warning-exit.cjs | 10 +- tests/corpus/5185-util-inherits.cjs | 81 ++ tests/corpus/5186-util-inherits.ts | 56 ++ tests/corpus/5187-util-deprecate.cjs | 86 ++ tests/corpus/5188-util-deprecate-flags.cjs | 43 + tests/corpus/5189-util-deprecate.ts | 41 + tests/corpus/5190-util-callbackify.cjs | 77 ++ tests/corpus/5191-util-callbackify.ts | 47 + tests/corpus/5192-util-debuglog.cjs | 53 ++ tests/corpus/5193-util-debuglog.ts | 45 + .../corpus/5194-process-emit-warning-tick.cjs | 37 + tests/fixtures/node-types/fenced.ts | 5 + tests/fixtures/node-types/util-callables.ts | 31 + .../__snapshots__/node-types-fenced.txt | 11 +- tests/harness/project-config.test.ts | 18 + 55 files changed, 2409 insertions(+), 178 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5185-util-inherits.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5186-util-inherits.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5187-util-deprecate.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5188-util-deprecate-flags.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5189-util-deprecate.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5190-util-callbackify.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5191-util-callbackify.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5192-util-debuglog.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5193-util-debuglog.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5194-process-emit-warning-tick.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/util-callables.ts.json create mode 100644 packages/runtime/src/scr_util_callable.c create mode 100644 tests/corpus/5185-util-inherits.cjs create mode 100644 tests/corpus/5186-util-inherits.ts create mode 100644 tests/corpus/5187-util-deprecate.cjs create mode 100644 tests/corpus/5188-util-deprecate-flags.cjs create mode 100644 tests/corpus/5189-util-deprecate.ts create mode 100644 tests/corpus/5190-util-callbackify.cjs create mode 100644 tests/corpus/5191-util-callbackify.ts create mode 100644 tests/corpus/5192-util-debuglog.cjs create mode 100644 tests/corpus/5193-util-debuglog.ts create mode 100644 tests/corpus/5194-process-emit-warning-tick.cjs create mode 100644 tests/fixtures/node-types/util-callables.ts diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index f8e2c0f1c..702d89c53 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -77,13 +77,15 @@ Some builtins can be stored and passed as functions: - Lowered `node:path` functions across the bare, POSIX, and win32 modules, including optional `basename` and variadic `join`/`resolve`. - Supported zero-argument `node:os` functions and `querystring.escape`/`unescape`. -- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, and `toUSVString`. +- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`. - `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`. Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile` and UTF-8 `fs.readFile`. `util.styleText` supports standard formats and aliases, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`. Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported. +`util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments. Class constructors passed to `util.inherits` or `util.deprecate` are refused. Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported. A stored `util.deprecate` reports a `length` of 4, where Node reports 3. In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused. `util.debuglog` reads `NODE_DEBUG` as the process started. Its `%s` refuses functions and objects with symbol-keyed members, `%o` refuses objects, and object arguments are refused on a color terminal. + `util.isDeepStrictEqual` supports primitives, ordinary records and arrays, cycles, enumerable string and symbol properties, and byte views. Its optional third argument skips prototype and constructor comparisons. Structural comparisons of native Maps, Sets, class instances, Errors, proxies, accessor properties, and function-valued own `constructor` properties remain refused. Synchronous and promise filesystem APIs with trailing arguments that affect behavior remain call-only. This includes writers, open, directory creation/removal, metadata readers, and directory readers. Passing them as values could discard modes, flags, encodings, or options during function-signature adaptation. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 58cbcd2ec..12b8e7594 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "716a56b02ac0127c5383", + "artifactVersion": "380f1d37ede0c65bb970", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 636e09051..3ee689db3 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -554,9 +554,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 36, + "partial": 41, "refused": 0, - "not-implemented": 83, + "not-implemented": 78, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -779,9 +779,9 @@ "apiEntries": 105, "static": { "supported": 0, - "partial": 47, + "partial": 53, "refused": 0, - "not-implemented": 58, + "not-implemented": 52, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -52841,9 +52841,9 @@ "anchor": "event-warning", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Emits string and Error warnings with types, codes, and details on the next tick, reporting to stderr before the 'warning' listeners run.", + "verification": "test-backed" } }, { @@ -53039,9 +53039,9 @@ "anchor": "processemitwarningwarning-options", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Emits string and Error warnings with types, codes, and details on the next tick, reporting to stderr before the 'warning' listeners run.", + "verification": "test-backed" } }, { @@ -53057,9 +53057,9 @@ "anchor": "processemitwarningwarning-type-code-ctor", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Emits string and Error warnings with types, codes, and details on the next tick, reporting to stderr before the 'warning' listeners run.", + "verification": "test-backed" } }, { @@ -54299,9 +54299,9 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Program assignments silence or throw deprecation warnings; compiled binaries take no Node command-line flags.", + "verification": "test-backed" } }, { @@ -54754,9 +54754,9 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Program assignments silence or throw deprecation warnings; compiled binaries take no Node command-line flags.", + "verification": "test-backed" } }, { @@ -68897,9 +68897,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Calls back on the next tick with results, rejection reasons, and ERR_FALSY_VALUE_REJECTION, forwarding receivers; typed callbacks must accept a null error.", + "verification": "test-backed" } }, { @@ -68943,9 +68943,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches sections against NODE_DEBUG as the process started and writes formatted SECTION PID lines to stderr; colored object output and %o over objects are refused.", + "verification": "test-backed" } }, { @@ -68966,9 +68966,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "The enabled getter tests the section on first read; the callback receives the section's implementation with its own enabled getter.", + "verification": "test-backed" } }, { @@ -68989,9 +68989,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Matches sections against NODE_DEBUG as the process started and writes formatted SECTION PID lines to stderr; colored object output and %o over objects are refused.", + "verification": "test-backed" } }, { @@ -69012,9 +69012,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Warns once per code or wrapper on the next tick, forwards receivers, arguments, and construction, and observes process.noDeprecation and process.throwDeprecation; class constructors are refused.", + "verification": "test-backed" } }, { @@ -69219,9 +69219,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Links constructor-function prototypes and sets super_, with Node's argument errors; class constructors are refused.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 35f26293b..4140ceabc 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 40, - "verify-gap": 2383, - "audit-partial": 783, + "verify-gap": 2372, + "audit-partial": 794, "classify": 2, "implement": 80 } @@ -52571,12 +52571,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:process.'warning'", + "tests": [ + "tests/corpus/2215-process-emit-warning-exit.cjs", + "tests/corpus/5194-process-emit-warning-tick.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs" + ] } } }, @@ -52811,12 +52815,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:process.process.emitWarning", + "tests": [ + "tests/corpus/2215-process-emit-warning-exit.cjs", + "tests/corpus/5194-process-emit-warning-tick.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs" + ] } } }, @@ -52831,12 +52839,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:process.process.emitWarning", + "tests": [ + "tests/corpus/2215-process-emit-warning-exit.cjs", + "tests/corpus/5194-process-emit-warning-tick.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs" + ] } } }, @@ -54259,12 +54271,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:process.noDeprecation", + "tests": [ + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/fixtures/node-types/util-callables.ts" + ] } } }, @@ -54782,12 +54798,16 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:process", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:process.throwDeprecation", + "tests": [ + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/fixtures/node-types/util-callables.ts" + ] } } }, @@ -70275,12 +70295,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.util.callbackify", + "tests": [ + "tests/corpus/5190-util-callbackify.cjs", + "tests/corpus/5191-util-callbackify.ts", + "tests/fixtures/node-types/util-callables.ts" + ] } } }, @@ -70325,12 +70349,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.util.debuglog", + "tests": [ + "tests/corpus/5192-util-debuglog.cjs", + "tests/corpus/5193-util-debuglog.ts", + "tests/fixtures/node-types/util-callables.ts" + ] } } }, @@ -70350,12 +70378,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:util.debuglog", + "tests": [ + "tests/corpus/5192-util-debuglog.cjs", + "tests/corpus/5193-util-debuglog.ts", + "tests/fixtures/node-types/util-callables.ts" + ] } } }, @@ -70375,12 +70407,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.util.debug", + "tests": [ + "tests/corpus/5192-util-debuglog.cjs", + "tests/corpus/5193-util-debuglog.ts" + ] } } }, @@ -70400,12 +70435,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.util.deprecate", + "tests": [ + "tests/corpus/5187-util-deprecate.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/fixtures/node-types/util-callables.ts" + ] } } }, @@ -70640,12 +70680,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:util", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.util.inherits", + "tests": [ + "tests/corpus/5185-util-inherits.cjs", + "tests/corpus/5186-util-inherits.ts", + "tests/fixtures/node-types/util-callables.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 2e808bd03..14b98b2bc 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -581,9 +581,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 36, + "partial": 41, "refused": 0, - "not-implemented": 83, + "not-implemented": 78, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -836,9 +836,9 @@ "apiEntries": 105, "static": { "supported": 0, - "partial": 47, + "partial": 53, "refused": 0, - "not-implemented": 58, + "not-implemented": 52, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -24086,7 +24086,7 @@ "nodeStability": null, "static": { "status": "not-applicable", - "evidence": "compiler-chapter-policy:deprecations" + "evidence": "compiler-feature:deprecations.util.debug" }, "anchorSource": "exact" }, @@ -72621,12 +72621,16 @@ "tests": [ "packages/compiler/test/process-builtin-values.test.ts", "tests/corpus/1443-signal-handlers.ts", + "tests/corpus/2215-process-emit-warning-exit.cjs", "tests/corpus/2314-process-introspection.ts", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts", "tests/corpus/2946-child-fork-ipc/main.ts", + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/corpus/5194-process-emit-warning-tick.cjs", "tests/corpus/990-process-basics.ts", "tests/corpus/node24-env-files-errors.ts", "tests/corpus/node24-env-files.ts", @@ -72645,6 +72649,7 @@ "tests/corpus/stdio-write-errors.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/fixtures/node-types/env-files.ts", + "tests/fixtures/node-types/util-callables.ts", "tests/harness/event-loop.test.ts", "tests/harness/project-config.test.ts" ] @@ -72907,8 +72912,13 @@ "anchor": "event-warning", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "compiler-feature:process.'warning'", + "tests": [ + "tests/corpus/2215-process-emit-warning-exit.cjs", + "tests/corpus/5194-process-emit-warning-tick.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs" + ] }, "anchorSource": "exact" }, @@ -73186,8 +73196,13 @@ "anchor": "processemitwarningwarning-options", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "compiler-feature:process.process.emitWarning", + "tests": [ + "tests/corpus/2215-process-emit-warning-exit.cjs", + "tests/corpus/5194-process-emit-warning-tick.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs" + ] }, "anchorSource": "exact" }, @@ -73204,8 +73219,13 @@ "anchor": "processemitwarningwarning-type-code-ctor", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "compiler-feature:process.process.emitWarning", + "tests": [ + "tests/corpus/2215-process-emit-warning-exit.cjs", + "tests/corpus/5194-process-emit-warning-tick.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs" + ] }, "anchorSource": "exact" }, @@ -74559,8 +74579,13 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "compiler-feature:process.noDeprecation", + "tests": [ + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/fixtures/node-types/util-callables.ts" + ] }, "anchorSource": "chapter" }, @@ -75054,8 +75079,13 @@ "anchor": "", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:process" + "status": "partial", + "evidence": "compiler-feature:process.throwDeprecation", + "tests": [ + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/fixtures/node-types/util-callables.ts" + ] }, "anchorSource": "chapter" }, @@ -92439,6 +92469,15 @@ "tests/corpus/2678-util-parseargs.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/5185-util-inherits.cjs", + "tests/corpus/5186-util-inherits.ts", + "tests/corpus/5187-util-deprecate.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/corpus/5190-util-callbackify.cjs", + "tests/corpus/5191-util-callbackify.ts", + "tests/corpus/5192-util-debuglog.cjs", + "tests/corpus/5193-util-debuglog.ts", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality.ts", @@ -92470,6 +92509,7 @@ "tests/fixtures/node-types/text-codecs.mts", "tests/fixtures/node-types/text-encoding.ts", "tests/fixtures/node-types/text-utilities.ts", + "tests/fixtures/node-types/util-callables.ts", "tests/fixtures/node-types/util-types.ts", "tests/harness/project-config.test.ts" ] @@ -93276,8 +93316,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.callbackify", + "tests": [ + "tests/corpus/5190-util-callbackify.cjs", + "tests/corpus/5191-util-callbackify.ts", + "tests/fixtures/node-types/util-callables.ts" + ] }, "anchorSource": "exact" }, @@ -93322,8 +93367,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.debuglog", + "tests": [ + "tests/corpus/5192-util-debuglog.cjs", + "tests/corpus/5193-util-debuglog.ts", + "tests/fixtures/node-types/util-callables.ts" + ] }, "anchorSource": "exact" }, @@ -93345,8 +93395,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "compiler-feature:util.debuglog", + "tests": [ + "tests/corpus/5192-util-debuglog.cjs", + "tests/corpus/5193-util-debuglog.ts", + "tests/fixtures/node-types/util-callables.ts" + ] }, "anchorSource": "exact" }, @@ -93368,8 +93423,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.debug", + "tests": [ + "tests/corpus/5192-util-debuglog.cjs", + "tests/corpus/5193-util-debuglog.ts" + ] }, "anchorSource": "exact" }, @@ -93391,8 +93450,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.deprecate", + "tests": [ + "tests/corpus/5187-util-deprecate.cjs", + "tests/corpus/5188-util-deprecate-flags.cjs", + "tests/corpus/5189-util-deprecate.ts", + "tests/fixtures/node-types/util-callables.ts" + ] }, "anchorSource": "exact" }, @@ -93616,8 +93681,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:util" + "status": "partial", + "evidence": "surface-manifest:node-builtin.util.inherits", + "tests": [ + "tests/corpus/5185-util-inherits.cjs", + "tests/corpus/5186-util-inherits.ts", + "tests/fixtures/node-types/util-callables.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index b6da19a4c..b4771169b 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -562,6 +562,18 @@ function publicDetail(tier) { ) { return "Reads filesystem capacity using string, Buffer, or file URL paths with number or BigInt fields and deferred callbacks; StatFs prototype behavior and complete filesystem error metadata remain unsupported."; } + if (source === "surface-manifest:node-builtin.util.inherits") { + return "Links constructor-function prototypes and sets super_, with Node's argument errors; class constructors are refused."; + } + if (source === "surface-manifest:node-builtin.util.deprecate") { + return "Warns once per code or wrapper on the next tick, forwards receivers, arguments, and construction, and observes process.noDeprecation and process.throwDeprecation; class constructors are refused."; + } + if (source === "surface-manifest:node-builtin.util.callbackify") { + return "Calls back on the next tick with results, rejection reasons, and ERR_FALSY_VALUE_REJECTION, forwarding receivers; typed callbacks must accept a null error."; + } + if (/^surface-manifest:node-builtin\.util\.debug(?:log)?$/.test(source)) { + return "Matches sections against NODE_DEBUG as the process started and writes formatted SECTION PID lines to stderr; colored object output and %o over objects are refused."; + } if (source.startsWith("surface-manifest:")) return "Implemented for the call shapes accepted by the compiler lowering."; if (/^compiler-dedicated:assert\.doesNot(?:Throw|Reject)$/.test(source)) { @@ -645,6 +657,18 @@ function publicDetail(tier) { ) { return "Native codec values support UTF-8 encoding and whole-buffer decoding with recognized literal labels and default options."; } + if (source === "compiler-feature:util.debuglog") { + return "The enabled getter tests the section on first read; the callback receives the section's implementation with its own enabled getter."; + } + if ( + source === "compiler-feature:process.process.emitWarning" || + source === "compiler-feature:process.'warning'" + ) { + return "Emits string and Error warnings with types, codes, and details on the next tick, reporting to stderr before the 'warning' listeners run."; + } + if (/^compiler-feature:process\.(?:no|throw)Deprecation$/.test(source)) { + return "Program assignments silence or throw deprecation warnings; compiled binaries take no Node command-line flags."; + } if (source === "compiler-feature:console.Console") { return "Instances over process.stdout, process.stderr, or stream writables support log, info, debug, error, and warn with Node's ignoreErrors handling; other options and methods remain refused."; } diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 772f355fd..7b75e237e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -127,6 +127,11 @@ "node-builtin.util.styleText": ["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"], "node-builtin.util.isDeepStrictEqual": ["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"], "node-builtin.util.types": ["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"], + "node-builtin.util.inherits": ["tests/corpus/5185-util-inherits.cjs", "tests/corpus/5186-util-inherits.ts", "tests/fixtures/node-types/util-callables.ts"], + "node-builtin.util.deprecate": ["tests/corpus/5187-util-deprecate.cjs", "tests/corpus/5188-util-deprecate-flags.cjs", "tests/corpus/5189-util-deprecate.ts", "tests/fixtures/node-types/util-callables.ts"], + "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": ["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"], @@ -153,6 +158,7 @@ { "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" }, + { "chapter": "deprecations", "symbols": ["util.debug"], "status": "not-applicable" }, { "chapter": "util", "symbols": ["util.stripVTControlCharacters", "util.toUSVString"], "status": "partial", "evidence": ["tests/corpus/node24-text-utilities.ts", "tests/corpus/node24-text-utilities-validation.js", "tests/corpus/node24-text-utilities-coercion.js", "tests/corpus/node24-text-utilities.cjs", "tests/fixtures/node-types/text-utilities.ts"] }, { "chapter": "util", "symbols": ["util.getSystemErrorName", "util.getSystemErrorMessage", "util.getSystemErrorMap"], "status": "partial", "evidence": ["tests/corpus/node24-system-errors.ts", "tests/corpus/node24-system-error-validation.js", "tests/corpus/node24-system-errors.cjs", "tests/fixtures/node-types/system-errors.ts"] }, { "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"] }, @@ -178,6 +184,9 @@ { "chapter": "process", "signatures": ["Event: `'uncaughtException'`", "Event: `'uncaughtExceptionMonitor'`"], "status": "partial", "evidence": ["tests/corpus/process-uncaught-sync.cjs", "tests/corpus/process-uncaught-loop.cjs", "tests/corpus/process-uncaught-module.mjs", "tests/corpus/process-uncaught-rejection.cjs", "tests/corpus/process-uncaught-monitor.cjs", "tests/corpus/process-uncaught-handler-throws.cjs"] }, { "chapter": "process", "signatures": ["`process.getBuiltinModule(id)`"], "status": "partial", "evidence": ["tests/corpus/process-builtin-values.js", "packages/compiler/test/process-builtin-values.test.ts"] }, { "chapter": "process", "signatures": ["`versions` Type: {Object}"], "status": "partial", "evidence": ["tests/corpus/process-versions-storage.ts"] }, + { "chapter": "process", "signatures": ["`process.emitWarning(warning[, type[, code]][, ctor])`", "`process.emitWarning(warning[, options])`", "Event: `'warning'`"], "status": "partial", "evidence": ["tests/corpus/2215-process-emit-warning-exit.cjs", "tests/corpus/5194-process-emit-warning-tick.cjs", "tests/corpus/5188-util-deprecate-flags.cjs"] }, + { "chapter": "process", "signatures": ["`noDeprecation` Type: {boolean}", "`throwDeprecation` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/5188-util-deprecate-flags.cjs", "tests/corpus/5189-util-deprecate.ts", "tests/fixtures/node-types/util-callables.ts"] }, + { "chapter": "util", "signatures": ["`debuglog().enabled`"], "status": "partial", "evidence": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts", "tests/fixtures/node-types/util-callables.ts"] }, { "chapter": "http", "signatures": ["Class: `http.ClientRequest`", "Class: `http.Server`", "Class: `http.ServerResponse`", "Class: `http.IncomingMessage`"], "status": "partial", "evidence": ["tests/corpus/2981-http-static-events.ts", "tests/corpus/2976-http-static-incoming-state.ts"] }, { "chapter": "http", "parent": "Class: `http.ClientRequest`", "signatures": ["`request.setSocketKeepAlive([enable][, initialDelay])`", "`request.setTimeout(timeout[, callback])`", "`destroyed` Type: {boolean}", "Event: `'timeout'`"], "status": "partial", "evidence": ["tests/corpus/2978-http-static-client-timeout.ts", "tests/corpus/2987-http-static-timeout-errors.ts"] }, { "chapter": "http", "parent": "Class: `http.ClientRequest`", "signatures": ["`request.abort()`", "`aborted` Type: {boolean}", "Event: `'abort'`"], "status": "partial", "evidence": ["tests/corpus/2988-http-static-client-abort.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 6622a46a9..523f0bd6b 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -235,6 +235,10 @@ declare var process: { env: { [name: string]: string | undefined }; /** Numeric writes set the status returned by ordinary program exit. */ exitCode?: number; + /* Silence and throw DeprecationWarnings (util.deprecate, + * process.emitWarning); undefined until assigned. */ + noDeprecation?: boolean; + throwDeprecation?: boolean; /* `never`, like @types/node: code behind an early-exit guard narrows * (`if (!x) process.exit(1)` proves x afterwards) — typed `void` the * guard narrows nothing and correct programs fail preflight. The @@ -2455,6 +2459,60 @@ declare module "util" { options?: { validateStream?: boolean; stream?: unknown }, ): string; export function parseEnv(content: string): { [key: string]: string | undefined }; + export function inherits(constructor: unknown, superConstructor: unknown): void; + export interface DeprecateOptions { + modifyPrototype?: boolean; + } + export function deprecate( + fn: T, + msg: string, + code?: string, + options?: DeprecateOptions, + ): T; + /* The callback receives null on success and the rejection reason + * otherwise (falsy reasons arrive as ERR_FALSY_VALUE_REJECTION). */ + export function callbackify( + fn: () => Promise, + ): (callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: () => Promise, + ): (callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1) => Promise, + ): (arg1: T1, callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: (arg1: T1) => Promise, + ): (arg1: T1, callback: (err: NodeJS.ErrnoException | null, result: TResult) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise, + ): (arg1: T1, arg2: T2, callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2) => Promise, + ): ( + arg1: T1, + arg2: T2, + callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, + ) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, + ): (arg1: T1, arg2: T2, arg3: T3, callback: (err: NodeJS.ErrnoException | null) => void) => void; + export function callbackify( + fn: (arg1: T1, arg2: T2, arg3: T3) => Promise, + ): ( + arg1: T1, + arg2: T2, + arg3: T3, + callback: (err: NodeJS.ErrnoException | null, result: TResult) => void, + ) => void; + export type DebugLoggerFunction = (msg: string, ...param: unknown[]) => void; + export interface DebugLogger extends DebugLoggerFunction { + enabled: boolean; + } + export function debuglog( + section: string, + callback?: (fn: DebugLoggerFunction) => void, + ): DebugLogger; + export { debuglog as debug }; export type TextEncoder = globalThis.TextEncoder; export type TextDecoder = globalThis.TextDecoder; export var TextEncoder: typeof globalThis.TextEncoder; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 4687d25a3..5df3667a9 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -66,6 +66,10 @@ export const LIB_FN_SYMS: Record = { "util.toUSVString": "scr_util_to_usv_string", "util.isDeepStrictEqual": "scr_util_is_deep_strict_equal", "util.styleText": "scr_util_style_text", + "util.inherits": "scr_util_inherits", + "util.deprecate": "scr_util_deprecate", + "util.callbackify": "scr_util_callbackify", + "util.debuglog": "scr_util_debuglog", "util.typeIs": "scr_dyn_util_type_is", "util.typeValue": "scr_dyn_util_type_value", "math.maxArr": "scr_math_max_arr", @@ -1022,6 +1026,8 @@ export const LIB_FN_SYMS: Record = { "process.offWarning": "scr_process_off_warning", "process.emitWarning": "scr_process_emit_warning", "process.emitWarningText": "scr_process_emit_warning_text", + "process.deprecationFlag": "scr_process_deprecation_flag", + "process.setDeprecationFlag": "scr_process_set_deprecation_flag", "process.onUncaughtException": "scr_process_on_uncaught_exception", "process.offUncaughtException": "scr_process_off_uncaught_exception", "process.onUnhandledRejection": "scr_process_on_unhandled_rejection", @@ -1112,7 +1118,12 @@ export const LIB_FN_SYMS: Record = { }; export const USES_TIMERS_LIB_FNS = new Set([ + "process.emitWarning", "process.emitWarningText", + // The wrappers schedule warnings and callbacks on the tick queue. + "util.deprecate", + "util.callbackify", + "util.debuglog", "worker.new", "worker.parentPort", "process.stdio", diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 5ff8b50d6..c323df39d 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -530,6 +530,9 @@ export async function compileCInternal( ...(opts.parseArgs && opts.inspect && opts.dynAsync ? [rt(join(rtDir, "scr_util_style.c"))] : []), + ...(opts.parseArgs && opts.inspect && opts.dynAsync && opts.dynInvoke && regex + ? [rt(join(rtDir, "scr_util_callable.c"))] + : []), ...(opts.stream ? [rt(join(rtDir, "scr_stream.c"))] : []), // The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD, // epoll on Linux, WSAPoll on Windows — each TU is empty off its diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 6a06078df..e2eadfe87 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -327,6 +327,18 @@ export function lowerProcessProperty( if (member === "versions") { return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc }; } + // process.noDeprecation / process.throwDeprecation: the values the + // program assigned (undefined before that — no Node flags reach a + // compiled binary). + if (member === "noDeprecation" || member === "throwDeprecation") { + return { + kind: "libCall", + fn: "process.deprecationFlag", + args: [numLit(member === "noDeprecation" ? 0 : 1, loc)], + type: DYN, + loc, + }; + } if (member === "version") { // Node's own version string for the compatibility target, the same // release process.versions.node names (the runtime's @@ -920,9 +932,9 @@ export function lowerProcessMethodCall( : "process.onRejectionHandled"; return { kind: "libCall", fn, args: isOff ? [cb] : [cb, onceArg], type: VOID, loc }; } - // 'warning': the listener crosses as a dyn function; emitWarning - // and the runtime deprecation sites dispatch synchronously - // (SEMANTICS.md). off/removeListener remove by closure identity. + // 'warning': the listener crosses as a dyn function; emitWarning and + // util.deprecate dispatch on the next tick. off/removeListener remove + // by closure identity. if (event === "warning" && (member === "on" || isOff)) { if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b85eecb29..1fa973d8e 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -7879,7 +7879,13 @@ export function lowerRecordFieldCall( if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee); // Inferred JavaScript option records can retain checked callables in // fields even when the checker only describes those members as any. - if (callee.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) { + // TypeScript reaches here for callable library types that are checked + // values (util.DebugLogger). + if ( + callee.type.kind === "dyn" && + (isJsSourceFile(call.getSourceFile()) || + lowerer.checker.getCallSignatures(lowerer.typeOf(access)).length > 0) + ) { const receiver = target && receiverLocal ? lowerer.coerceToExpected(target.obj, DYN) : undefined; const spread = call.arguments.some(ts.isSpreadElement) diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 5b4204659..634534b98 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -15,6 +15,7 @@ import { RUNTIME_ERROR_CLASSES, STRING, UNDEFINED_T, + VOID, type SrcLoc, canConvertToDyn, canDynCheckTo, @@ -1937,6 +1938,12 @@ export function lowerUtilModuleCall( loc, }; } + case "inherits": + case "deprecate": + case "callbackify": + case "debuglog": + case "debug": + return lowerUtilCallableCall(lowerer, expr, bi.member, loc); case "getSystemErrorMap": { if (expr.arguments.some(ts.isSpreadElement)) lowerer.noLowering("util.getSystemErrorMap with spread arguments", expr); @@ -2027,6 +2034,86 @@ export function lowerUtilModuleCall( } } +/** util.inherits/deprecate/callbackify/debuglog: every argument crosses as + * a checked-dynamic value, evaluated left to right before the runtime call + * (surplus arguments are evaluated and dropped). The function helpers + * answer native dyn wrappers; a static function slot checks the wrapper + * when it receives it, and dyn slots keep calling it with JS arity. */ +function lowerUtilCallableCall( + lowerer: Lowerer, + expr: ts.CallExpression, + member: string, + loc: SrcLoc, +): IrExpr { + if (expr.arguments.some(ts.isSpreadElement)) + lowerer.noLowering(`util.${member} with spread arguments`, expr); + const fn = + member === "inherits" + ? "util.inherits" + : member === "deprecate" + ? "util.deprecate" + : member === "callbackify" + ? "util.callbackify" + : "util.debuglog"; + const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2; + // Program classes keep static prototypes: there is no function object + // whose prototype chain the runtime could relink or wrap. + const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0; + for (const arg of expr.arguments.slice(0, classArgs)) { + if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval") + lowerer.noLowering( + `util.${member} over a class constructor`, + arg, + member === "inherits" + ? "declare the subclass with extends" + : "deprecate a factory function or the class's call sites instead", + ); + } + if (member === "callbackify" && callbackifyErrorExcludesNull(lowerer, expr)) + lowerer.noLowering( + "util.callbackify into a callback whose error parameter excludes null", + expr, + "Node calls back with a null error on success; type the binding's callback as (err: NodeJS.ErrnoException | null, ...) => void", + ); + const stmts: IrStmt[] = []; + const args: IrExpr[] = []; + for (let i = 0; i < arity; i++) { + const input: IrExpr = expr.arguments[i] + ? lowerer.lowerExprExpecting(expr.arguments[i]!, DYN) + : dynUndefinedExpr(loc); + const saved = lowerer.declareHiddenLocal("%utilInput", DYN); + stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); + args.push(varRef(saved.id, DYN, loc)); + } + for (const arg of expr.arguments.slice(arity)) + stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc }); + const type = fn === "util.inherits" ? VOID : DYN; + const result: IrExpr = { kind: "libCall", fn, args, type, loc }; + return { kind: "seqExpr", stmts, result, type: result.type, loc }; +} + +/** Whether the slot receiving a callbackify wrapper declares a callback + * whose first parameter cannot hold null. Some @types/node overloads type + * that error as NodeJS.ErrnoException, but Node passes null on success, and + * a statically typed callback parameter cannot receive a value its type + * excludes. Untyped (JavaScript or unknown) slots keep the wrapper dyn. */ +function callbackifyErrorExcludesNull(lowerer: Lowerer, expr: ts.CallExpression): boolean { + if (isJsSourceFile(expr.getSourceFile())) return false; + const checker = lowerer.checker; + const slot = checker.getContextualType(expr) ?? lowerer.typeOf(expr); + const wrapper = checker.getCallSignatures(slot)[0]; + const callbackSymbol = wrapper?.getParameters().at(-1); + if (!callbackSymbol) return false; + const callback = checker.getCallSignatures(checker.getTypeOfSymbol(callbackSymbol))[0]; + const errorSymbol = callback?.getParameters()[0]; + if (!errorSymbol) return false; + const error = checker.getTypeOfSymbol(errorSymbol); + const arms = error.isUnionType() ? ts.constituentTypes(error) : [error]; + return !arms.some( + (arm) => (arm.flags & (ts.TypeFlags.Null | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + ); +} + /** util.parseArgs(config?): the configuration is ordinary JSON-safe data, * so it crosses into the runtime as a checked-dynamic tree. The native * parser returns the same tree-shaped result Node does (including token diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 8341e3446..f7ab90ae0 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6336,6 +6336,24 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm loc, }; } + if (!expr.left.questionDotToken) { + const flag = lowerer.stdlibGlobalMember(expr.left, "process"); + if (flag === "noDeprecation" || flag === "throwDeprecation") { + const loc = locOf(expr); + const value = lowerer.lowerExprExpecting(expr.right, DYN); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "process.setDeprecationFlag", + args: [numLit(flag === "noDeprecation" ? 0 : 1, loc), value], + type: VOID, + loc, + }, + loc, + }; + } + } // `r._read = fn` / `w._write = fn` (and the other underscore // methods) on a runtime-stream-rooted receiver: Node's // own-property shadow of the prototype method — the runtime diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 342b32dac..4f136f0d1 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1483,6 +1483,53 @@ export const BUILTIN_MODULE_FNS: Record< result: STRING, valueParams: exactValueParams(DYN), }, + // The function helpers take checked-dynamic arguments and answer native + // dyn functions, which a static function slot checks when it receives + // one (lowerUtilCallableCall in the util spoke). + inherits: { + fn: "util.inherits", + params: [DYN, DYN], + result: VOID, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + ], + }, + deprecate: { + fn: "util.deprecate", + params: [DYN, DYN, DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + callbackify: { + fn: "util.callbackify", + params: [DYN], + result: DYN, + valueParams: [{ mode: "required", type: DYN }], + }, + debuglog: { + fn: "util.debuglog", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, + debug: { + fn: "util.debuglog", + params: [DYN, DYN], + result: DYN, + valueParams: [ + { mode: "required", type: DYN }, + { mode: "optional", type: DYN, defaultValue: undefined }, + ], + }, }, // The util/types spoke supplies the probe name to the shared brand check. "util/types": { diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 93a923078..f3ef6e9bf 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -139,9 +139,13 @@ export const FETCH_AMBIENT_TYPES = [ "ReadableStreamReadDoneResult", ] as const; -/** node:util.parseArgs's public and @types/node helper type names. Values +/** node:util.parseArgs's public and @types/node helper type names, plus + * util.debuglog's DebugLogger (a native function value with an `enabled` + * accessor) and the DebugLoggerFunction its callback receives. Values * behind this surface use the checked-dynamic tree; see mapTypeInner. */ const PARSE_ARGS_DYN_TYPES = new Set([ + "DebugLogger", + "DebugLoggerFunction", "ParseArgsConfig", "ParseArgsOptionDescriptor", "ParseArgsOptionsConfig", diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 287ef1fe8..a57620cdd 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -280,6 +280,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "process.kill", "process.killNum", "process.setExitCode", + // Node's argument grammar throws ERR_INVALID_ARG_TYPE at the call. + "process.emitWarning", // cpuUsage(prev)'s field validation: negative/non-finite prev fields // throw Node's ERR_INVALID_ARG_VALUE RangeError, catchably. "process.cpuPrevValidate", @@ -369,6 +371,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "util.isDeepStrictEqual", "util.styleText", "util.toUSVString", + "util.inherits", + "util.deprecate", + "util.callbackify", + "util.debuglog", // The URI codecs throw the spec's URIError: decoding on bad hex/invalid // UTF-8 octets, encoding (and querystring.escape) on a lone surrogate. "str.decodeUriComponent", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 4fe8f4883..0e2e0657a 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -834,6 +834,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "util.toUSVString": { argTypes: [DYN], result: STRING }, "util.isDeepStrictEqual": { argTypes: [DYN, DYN, DYN], result: BOOL }, "util.styleText": { argTypes: [DYN, DYN, DYN], result: STRING }, + /** util.inherits/deprecate/callbackify/debuglog over checked-dynamic + * arguments (scr_util_callable.c). The function results are native dyn + * wrappers; call sites with a static function type check them back. */ + "util.inherits": { argTypes: [DYN, DYN], result: VOID }, + "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, + "util.callbackify": { argTypes: [DYN], result: DYN }, + "util.debuglog": { argTypes: [DYN, DYN], result: DYN }, "util.typeIs": { argTypes: [DYN, STRING], result: BOOL }, "util.typeValue": { argTypes: [DYN, STRING], result: DYN }, /** fs.statSync → a Stats value (may throw, like the other sync fs @@ -3307,11 +3314,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * Node's payload. */ "process.onRejectionHandled": { argTypes: [DYN, BOOL], result: VOID }, "process.offRejectionHandled": { argTypes: [DYN], result: VOID }, - /** process warnings (scr_lib.c): onWarning/offWarning register dyn - * listeners; emitWarning applies Node's full argument grammar over the - * call's dyn argument vector (ERR_INVALID_ARG_TYPE TypeErrors — MAY - * THROW; a listener throw propagates too) and always prints Node's - * stderr report. Emission is synchronous (SEMANTICS.md). */ + /** process warnings (scr_async_dyn.c): onWarning/offWarning register + * dyn listeners; emitWarning applies Node's full argument grammar over + * the call's dyn argument vector (ERR_INVALID_ARG_TYPE TypeErrors — MAY + * THROW) and emits on the next tick, like Node: Node's stderr report, + * then the listeners. */ "process.onWarning": { argTypes: [DYN], result: VOID }, "process.offWarning": { argTypes: [DYN], result: VOID }, "process.emitWarning": { argTypes: [DYN], result: VOID }, @@ -3319,6 +3326,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * members' warnings), emitted on the next tick as Node's emitWarning * does (so the loop runs); an empty message emits nothing. */ "process.emitWarningText": { argTypes: [STRING], result: VOID }, + /** process.noDeprecation (0) / process.throwDeprecation (1): the stored + * value (undefined until assigned) and its assignment. util.deprecate's + * warnings and DeprecationWarning emissions read their truthiness. */ + "process.deprecationFlag": { argTypes: [F64], result: DYN }, + "process.setDeprecationFlag": { argTypes: [F64, DYN], result: VOID }, /** AsyncLocalStorage (node:async_hooks — scr_async.c): stores are f64 * handles; contexts are immutable fiber-carried snapshots (spawned * fibers inherit the spawner's — Node's init-time capture). run/exit diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 8f99cce4a..e7ebee9fe 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -53,7 +53,13 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "process.offWarning", "process.emitWarning", "process.emitWarningText", + "process.deprecationFlag", + "process.setDeprecationFlag", "util.styleText", + "util.inherits", + "util.deprecate", + "util.callbackify", + "util.debuglog", "als.new", "als.get", "als.run", @@ -65,6 +71,13 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "dc.chanRunStores", ]); +const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([ + "util.inherits", + "util.deprecate", + "util.callbackify", + "util.debuglog", +]); + /** Inspect types and expressions together in one traversal. This is a fresh * snapshot: callers may mutate an IR module between compilation passes. */ export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures { @@ -200,6 +213,14 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run if (fn === "util.parseArgs" || fn === "util.isDeepStrictEqual" || fn === "util.styleText") features.parseArgs = true; if (fn === "util.styleText") features.processEvents = true; + // scr_util_callable.c: dyn wrappers over the descriptor, inspect, + // regex (NODE_DEBUG matching) and terminal-color units. + if (UTIL_CALLABLE_LIB_FNS.has(fn)) { + features.parseArgs = true; + features.inspect = true; + features.dynInvoke = true; + features.regex = true; + } if (fn.startsWith("fs.watch") || fn.startsWith("watcher.")) features.fsWatch = true; if (fn.startsWith("test.")) features.nodeTest = true; if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 2e372fd3a..4f2ab80d1 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -2123,6 +2123,30 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.util.callbackify", + "kind": "node-builtin", + "name": "util.callbackify", + "status": "static" + }, + { + "id": "node-builtin.util.debug", + "kind": "node-builtin", + "name": "util.debug", + "status": "static" + }, + { + "id": "node-builtin.util.debuglog", + "kind": "node-builtin", + "name": "util.debuglog", + "status": "static" + }, + { + "id": "node-builtin.util.deprecate", + "kind": "node-builtin", + "name": "util.deprecate", + "status": "static" + }, { "id": "node-builtin.util.getSystemErrorMap", "kind": "node-builtin", @@ -2141,6 +2165,12 @@ "name": "util.getSystemErrorName", "status": "static" }, + { + "id": "node-builtin.util.inherits", + "kind": "node-builtin", + "name": "util.inherits", + "status": "static" + }, { "id": "node-builtin.util.isDeepStrictEqual", "kind": "node-builtin", diff --git a/packages/compiler/test/cc-driver.test.ts b/packages/compiler/test/cc-driver.test.ts index 8ae06c524..b86b5568e 100644 --- a/packages/compiler/test/cc-driver.test.ts +++ b/packages/compiler/test/cc-driver.test.ts @@ -276,6 +276,11 @@ test.skipIf(!zigOnPath())( options: { parseArgs: true, inspect: true, dynAsync: true, events: true }, call: "scr_str_release(scr_util_style_text(scr_dyn_undefined(), scr_dyn_undefined(), scr_dyn_undefined()))", }, + { + name: "callable", + options: { parseArgs: true, inspect: true, dynAsync: true, dynInvoke: true, regex: true }, + call: "scr_dyn_release(scr_util_debuglog(scr_dyn_undefined(), scr_dyn_undefined()))", + }, ]; try { await withCcEnv("zigcc", "x86_64-windows-gnu", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5185-util-inherits.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5185-util-inherits.cjs.json new file mode 100644 index 000000000..c952d473e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5185-util-inherits.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5185-util-inherits.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5186-util-inherits.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5186-util-inherits.ts.json new file mode 100644 index 000000000..de897ac41 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5186-util-inherits.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5186-util-inherits.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5187-util-deprecate.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5187-util-deprecate.cjs.json new file mode 100644 index 000000000..b6d51030c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5187-util-deprecate.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5187-util-deprecate.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5188-util-deprecate-flags.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5188-util-deprecate-flags.cjs.json new file mode 100644 index 000000000..9e951c015 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5188-util-deprecate-flags.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5188-util-deprecate-flags.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5189-util-deprecate.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5189-util-deprecate.ts.json new file mode 100644 index 000000000..bcb1597bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5189-util-deprecate.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5189-util-deprecate.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5190-util-callbackify.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5190-util-callbackify.cjs.json new file mode 100644 index 000000000..e00593695 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5190-util-callbackify.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5190-util-callbackify.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5191-util-callbackify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5191-util-callbackify.ts.json new file mode 100644 index 000000000..9ebe09332 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5191-util-callbackify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5191-util-callbackify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5192-util-debuglog.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5192-util-debuglog.cjs.json new file mode 100644 index 000000000..07f1a648d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5192-util-debuglog.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5192-util-debuglog.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5193-util-debuglog.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5193-util-debuglog.ts.json new file mode 100644 index 000000000..b8d8a098c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5193-util-debuglog.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5193-util-debuglog.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5194-process-emit-warning-tick.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5194-process-emit-warning-tick.cjs.json new file mode 100644 index 000000000..1ce4b0569 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5194-process-emit-warning-tick.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5194-process-emit-warning-tick.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/util-callables.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/util-callables.ts.json new file mode 100644 index 000000000..511b17686 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/util-callables.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/util-callables.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index b56f63e76..3ff9d6347 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -76,6 +76,7 @@ const optional = [ // Utility objects must only reference runtime families selected with them. ["scr_util_compare.c", all("parseArgs", "symbol")], ["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")], + ["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")], ["scr_stream.c", "stream"], ["scr_loop_kqueue.c", any("netEffective", "dgram")], ["scr_loop_epoll.c", any("netEffective", "dgram")], diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index f81d5ff6d..cd71a61c7 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -66,6 +66,7 @@ const OPTIONAL = [ // Utility objects must only reference runtime families selected with them. ["scr_util_compare.c", all("parseArgs", "symbol")], ["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")], + ["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")], ["scr_stream.c", "stream"], ["scr_loop_kqueue.c", any("netEffective", "dgram")], ["scr_loop_epoll.c", any("netEffective", "dgram")], diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 24617f054..12af537e7 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -596,11 +596,11 @@ ScrDyn *scr_await_dyn_value_settled(ScrDyn *value) { /* ── process warnings (emitWarning + the 'warning' event) ───────────── * Gated with the rest of this TU (a deprecation-emitting unit's gate - * must imply the dynAsync link). Listeners are dyn functions; emission - * is SYNCHRONOUS at the - * call (Node defers a tick through nextTick — the MaxListenersExceeded - * precedent, SEMANTICS.md 138) and the default stderr report always - * prints (Node's own bootstrap listener; a compiled binary has no + * must imply the dynAsync link). Listeners are dyn functions; the + * warning is built at the call and emitted on the next tick, as Node's + * emitWarning does (scr_warning_emit_deferred). The default stderr report + * prints for every warning except deprecations under process.noDeprecation + * (Node's own bootstrap listener; a compiled binary has no * --no-warnings). The warning VALUE is the dyn error encoding built over * an ScrError (identity-cached, so a listener comparing two deliveries * of one warning sees one object); a string `detail` joins the dyn node @@ -648,14 +648,71 @@ void scr_process_off_warning(ScrDyn *fn) { } } +/* process.noDeprecation (0) and process.throwDeprecation (1): ordinary + * writable process properties, undefined until the program assigns them. + * A compiled binary takes no Node command-line flags, so only those + * assignments set them. */ +static SCR_TL ScrDyn *scr_deprecation_flags[2] = {NULL, NULL}; +static SCR_TL bool scr_deprecation_flags_registered = false; + +static void scr_deprecation_flags_teardown(void) { + for (size_t i = 0; i < 2; i++) { + scr_dyn_release(scr_deprecation_flags[i]); + scr_deprecation_flags[i] = NULL; + } +} + +ScrDyn *scr_process_deprecation_flag(double which) { + ScrDyn *value = scr_deprecation_flags[which != 0]; + return scr_dyn_retain(value ? value : scr_dyn_undefined()); +} + +void scr_process_set_deprecation_flag(double which, ScrDyn *value) { + if (!scr_deprecation_flags_registered) { + scr_deprecation_flags_registered = true; + scr_atexit(scr_deprecation_flags_teardown); + } + size_t slot = which != 0; + ScrDyn *previous = scr_deprecation_flags[slot]; + scr_deprecation_flags[slot] = scr_dyn_retain(value); + scr_dyn_release(previous); +} + +bool scr_process_deprecation_flag_on(int which) { + const ScrDyn *value = scr_deprecation_flags[which != 0]; + return value != NULL && scr_dyn_truthy(value); +} + +/* The warning's name as warning.toString() reads it: the own property, or + * the Error class name an emitted Error instance inherits. +1 or NULL. */ +static ScrStr *scr_warning_name(const ScrDyn *w) { + const ScrDyn *en = scr_dyn_obj_get(w, "name", 4); + if (en) return en->kind == SCR_DYN_STR ? scr_str_retain(en->v.str) : NULL; + ScrError *e = scr_errdyn_err_of(w); + if (!e) return NULL; + ScrStr *name = e->name ? scr_str_retain(e->name) : scr_error_default_name(e); + scr_error_release(e); + return name; +} + +static bool scr_warning_is_deprecation(const ScrDyn *w) { + ScrStr *name = scr_warning_name(w); + bool deprecation = name && name->len == 18 && memcmp(name->data, "DeprecationWarning", 18) == 0; + scr_str_release(name); + return deprecation; +} + /* The default stderr report + dispatch over a built warning dyn node. * Borrowed. A listener throw propagates (the dc publish stance). */ static void scr_warning_dispatch(ScrDyn *w) { /* "(node:pid) [CODE] Name: message" + "\n" — Node's * onWarning report, written as ONE console.error line (so the global * console's group indentation applies to each of its lines). Node - * registers that listener at startup, so it runs before the program's. */ - const ScrDyn *en = scr_dyn_obj_get(w, "name", 4); + * registers that listener at startup, so it runs before the program's, + * and skips deprecations while process.noDeprecation is set. */ + bool deprecation = scr_warning_is_deprecation(w); + if (deprecation && scr_process_deprecation_flag_on(0)) goto listeners; + ScrStr *name = scr_warning_name(w); const ScrDyn *em = scr_dyn_obj_get(w, "message", 7); const ScrDyn *ec = scr_dyn_obj_get(w, "code", 4); const ScrDyn *ed = scr_dyn_obj_get(w, "detail", 6); @@ -669,24 +726,29 @@ static void scr_warning_dispatch(ScrDyn *w) { scr_jb_put_str(&report, ec->v.str); scr_jb_puts(&report, "] "); } - if (en && en->kind == SCR_DYN_STR) scr_jb_put_str(&report, en->v.str); + if (name) scr_jb_put_str(&report, name); else scr_jb_puts(&report, "Warning"); + scr_str_release(name); scr_jb_puts(&report, ": "); if (em && em->kind == SCR_DYN_STR) scr_jb_put_str(&report, em->v.str); if (ed && ed->kind == SCR_DYN_STR) { scr_jb_puts(&report, "\n"); scr_jb_put_str(&report, ed->v.str); } - /* Node's one-time trace hint, after the first report. */ + /* Node's one-time trace hint, after the first report; deprecations + * name their own trace flag. */ static SCR_TL bool hinted = false; if (!hinted) { hinted = true; - scr_jb_puts(&report, "\n(Use `node --trace-warnings ...` to show where the warning was created)"); + scr_jb_puts(&report, deprecation + ? "\n(Use `node --trace-deprecation ...` to show where the warning was created)" + : "\n(Use `node --trace-warnings ...` to show where the warning was created)"); } ScrLogArg line = {.tag = SCR_ARG_STR}; line.v.s = scr_jb_finish(&report); scr_console_error(1, &line); scr_str_release(line.v.s); +listeners: for (size_t i = 0; i < scr_nwarn; i++) { ScrDyn *r = scr_dyn_call(scr_warn_listeners[i], &w, 1, "listener"); if (r == NULL) return; /* pending exception propagates */ @@ -694,20 +756,63 @@ static void scr_warning_dispatch(ScrDyn *w) { } } -/* A warning built from C parts (the runtime deprecation sites): name - * defaults to "Warning"; code/detail optional. Borrows message. */ -void scr_emit_warning(const char *name, const char *code, ScrStr *message) { +/* A string warning with a type and an optional code, as + * process.emitWarning(message, type, code) builds it. Borrows; +1. */ +ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code) { ScrError *e = scr_error_new(SCR_ERR_ERROR, message); scr_str_release(e->name); - e->name = scr_str_new(name ? name : "Warning", strlen(name ? name : "Warning")); + e->name = scr_str_new(type, strlen(type)); e->name_present = true; - if (code) e->code = scr_str_new(code, strlen(code)); + if (code) e->code = scr_str_retain((ScrStr *)code); ScrDyn *w = scr_dyn_from_error(e); scr_error_release(e); + return w; +} + +/* A warning built from C parts (the runtime deprecation sites): name + * defaults to "Warning"; code/detail optional. Reported synchronously. + * Borrows message. */ +void scr_emit_warning(const char *name, const char *code, ScrStr *message) { + ScrStr *code_text = code ? scr_str_new(code, strlen(code)) : NULL; + ScrDyn *w = scr_warning_new(message, name ? name : "Warning", code_text); + scr_str_release(code_text); scr_warning_dispatch(w); scr_dyn_release(w); } +/* Node's emitWarning tail for runtime-built warnings: deprecations are + * dropped while process.noDeprecation is set and thrown from their own + * tick while process.throwDeprecation is set; every other warning reaches + * the report and the 'warning' listeners on the next tick. */ +static void scr_warning_tick(ScrClosure *c) { + ScrDyn *w = scr_box_get_ref(c->caps[0]); + scr_warning_dispatch(w); + scr_dyn_release(w); +} + +static void scr_warning_throw_tick(ScrClosure *c) { + ScrDyn *w = scr_box_get_ref(c->caps[0]); + scr_throw_ref(w, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); +} + +static void scr_warning_defer(ScrDyn *w, void (*fire)(ScrClosure *)) { + ScrClosure *c = scr_closure_new((void *)fire, 1); + c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(c->caps[0], scr_dyn_retain(w)); + scr_next_tick(c); +} + +void scr_warning_emit_deferred(ScrDyn *w) { + if (scr_warning_is_deprecation(w)) { + if (scr_process_deprecation_flag_on(0)) return; + if (scr_process_deprecation_flag_on(1)) { + scr_warning_defer(w, scr_warning_throw_tick); + return; + } + } + scr_warning_defer(w, scr_warning_tick); +} + /* The console members' warnings. Node's emitWarning emits on * process.nextTick, so these queue in order and each rides one raw tick * on the shared tick queue; any left at exit are dropped with it. */ @@ -760,7 +865,8 @@ static void scr_warn_bad_arg(const char *arg, const char *want) { * values: (warning: string | Error), then for string warnings a type * string / ctor function / options object ({type, code, detail}) second * and a code string / ctor function third. Wrong kinds throw Node's - * ERR_INVALID_ARG_TYPE TypeErrors; non-string details are ignored. */ + * ERR_INVALID_ARG_TYPE TypeErrors at the call; non-string details are + * ignored. The warning itself is emitted on the next tick. */ void scr_process_emit_warning(ScrDyn *args) { size_t argc = args->kind == SCR_DYN_ARR ? args->v.arr.len : 0; ScrDyn *const *items = args->kind == SCR_DYN_ARR ? args->v.arr.items : NULL; @@ -768,7 +874,7 @@ void scr_process_emit_warning(ScrDyn *args) { /* An Error-encoded warning: type/code arguments are ignored (Node). */ if (warning != NULL && warning->kind == SCR_DYN_OBJ && scr_dyn_obj_get(warning, "%error", 6) != NULL) { - scr_warning_dispatch(warning); + scr_warning_emit_deferred(warning); return; } if (warning == NULL || warning->kind != SCR_DYN_STR) { @@ -823,7 +929,7 @@ void scr_process_emit_warning(ScrDyn *args) { if (detail) { scr_dyn_obj_set(w, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */ } - scr_warning_dispatch(w); + scr_warning_emit_deferred(w); scr_dyn_release(w); } diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 98cc8478d..73ab6d194 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2973,7 +2973,16 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) { switch (cb->kind) { case SCR_DYN_NULL: d = "null"; break; case SCR_DYN_UNDEF: d = "undefined"; break; - case SCR_DYN_OBJ: d = "an instance of Object"; break; + case SCR_DYN_OBJ: + if (cb->null_proto) { + /* No constructor name: Node renders inspect(value, { depth: -1 }). */ + bool keys = false; + for (size_t i = 0; i < cb->v.obj.len && !keys; i++) keys = cb->v.obj.entries[i].enumerable; + d = keys ? "[Object: null prototype]" : "[Object: null prototype] {}"; + } else { + d = "an instance of Object"; + } + break; case SCR_DYN_ARR: d = "an instance of Array"; break; case SCR_DYN_BYTES: snprintf(detail, cap, "an instance of %s", cb->buffer ? "Buffer" : cb->v.bytes->is_data_view ? "DataView" : scr_bytes_elem_name(cb->v.bytes->elem)); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index de6b37189..733f1ca68 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -738,10 +738,41 @@ ScrStr *scr_env_get(const ScrStr *name) { #endif } +/* util.debuglog matches sections against NODE_DEBUG as the process + * started, so the first write to that variable records the value it + * replaces. */ +static SCR_TL bool scr_node_debug_captured = false; +static SCR_TL char *scr_node_debug_value = NULL; + +static void scr_node_debug_teardown(void) { + free(scr_node_debug_value); + scr_node_debug_value = NULL; +} + +const char *scr_node_debug_initial(void) { + if (!scr_node_debug_captured) { + scr_node_debug_captured = true; + const char *value = scr_getenv("NODE_DEBUG"); + if (value) { + size_t len = strlen(value); + scr_node_debug_value = malloc(len + 1); + if (!scr_node_debug_value) scr_trap("scriptc: out of memory\n"); + memcpy(scr_node_debug_value, value, len + 1); + scr_atexit(scr_node_debug_teardown); + } + } + return scr_node_debug_value; +} + +static void scr_node_debug_capture(const ScrStr *name) { + if (name->len == 10 && memcmp(name->data, "NODE_DEBUG", 10) == 0) (void)scr_node_debug_initial(); +} + /* process.env.NAME = v — setenv(3): later scr_env_get reads and spawned * children (posix_spawn inherits environ) observe the write, like Node. * Both args borrowed (NUL-terminated ScrStr data); setenv copies. */ void scr_env_set(const ScrStr *name, const ScrStr *value) { + scr_node_debug_capture(name); /* The environment holds UTF-8: a lone surrogate is stored as U+FFFD. */ char *name_owned, *value_owned; const char *k = scr_str_utf8(name, &name_owned), *v = scr_str_utf8(value, &value_owned); @@ -765,6 +796,7 @@ done: /* `delete process.env.NAME` — unsetenv(3): later reads answer absent and * spawned children lose the variable, like Node. Borrowed. */ void scr_env_unset(const ScrStr *name) { + scr_node_debug_capture(name); #ifdef SCR_WORKERS if (scr_context_env_set(name->data, NULL)) return; #endif diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 9a5a23e58..fe1d9f182 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -1634,6 +1634,17 @@ void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep, * Borrows config; returns +1, or NULL with a Node-coded TypeError pending. */ struct ScrDyn *scr_util_parse_args(const struct ScrDyn *config); ScrStr *scr_util_style_text(const struct ScrDyn *format, const struct ScrDyn *text, const struct ScrDyn *options); +/* Node's terminal color decision (FORCE_COLOR, NO_COLOR, TERM, CI hosts) + * once the stream is a terminal (scr_util_style.c). */ +bool scr_util_terminal_colors(void); +/* util.inherits, util.deprecate, util.callbackify and util.debuglog + * (scr_util_callable.c). Arguments are borrowed; function results are +1 + * native dyn functions, or NULL with Node's TypeError pending. */ +void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor); +struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code, + struct ScrDyn *options); +struct ScrDyn *scr_util_callbackify(struct ScrDyn *original); +struct ScrDyn *scr_util_debuglog(struct ScrDyn *section, struct ScrDyn *callback); /* Node/libuv system error utilities. Arguments are checked without coercion; * entries is a fresh array of [number, [name, message]] pairs (+1). */ @@ -3497,6 +3508,8 @@ ScrStr *scr_env_get(const ScrStr *name); * children observe the write, like Node. Both borrowed. Never throws. */ void scr_env_set(const ScrStr *name, const ScrStr *value); void scr_env_unset(const ScrStr *name); /* delete process.env.NAME */ +/* NODE_DEBUG as the process started (NULL when unset), for util.debuglog. */ +const char *scr_node_debug_initial(void); /* The whole environment as alternating [k, v, k, v, ...] strings in * environ order — the compiler's process.env snapshot builds its record * from this. +1 fresh array. Never throws. */ @@ -6030,15 +6043,28 @@ void scr_promise_mark_handled(ScrPromise *p); ScrDyn *scr_promise_reason_dyn(const ScrPromise *p); /* process warnings (scr_lib.c — always linked so any unit can emit a * deprecation): dyn listeners plus Node's default stderr report - * ("(node:pid) [CODE] Name: message" and a detail second line). Emission - * is SYNCHRONOUS at the call (Node defers a tick — SEMANTICS.md 138's - * precedent). scr_process_emit_warning takes the ARGUMENT VECTOR as one - * dyn array and applies Node's full grammar/TypeErrors; scr_emit_warning - * is the C-side deprecation entry. */ + * ("(node:pid) [CODE] Name: message" and a detail second line). + * scr_process_emit_warning takes the ARGUMENT VECTOR as one dyn array, + * applies Node's full grammar/TypeErrors at the call and emits on the + * next tick, as Node does; scr_emit_warning is the C-side entry and + * reports synchronously. */ void scr_process_on_warning(ScrDyn *fn); void scr_process_off_warning(ScrDyn *fn); void scr_process_emit_warning(ScrDyn *args); void scr_emit_warning(const char *name, const char *code, ScrStr *message); +/* process.noDeprecation (which 0) and process.throwDeprecation (which 1): + * the stored value (+1; undefined until assigned), the assignment + * (borrowed), and the truthiness the warning paths test. */ +ScrDyn *scr_process_deprecation_flag(double which); +void scr_process_set_deprecation_flag(double which, ScrDyn *value); +bool scr_process_deprecation_flag_on(int which); +/* Node's emitWarning tail on the next tick (util.deprecate): a + * DeprecationWarning is dropped under noDeprecation and thrown from its + * own tick under throwDeprecation; otherwise the report and the 'warning' + * listeners run. scr_warning_new builds the string form's warning value + * (borrows; +1). */ +void scr_warning_emit_deferred(ScrDyn *warning); +ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code); /* The promise-or-absent await's unit arm: one microtask hop, like JS's * await of any non-thenable (and like awaiting a settled promise). */ void scr_await_hop(void); diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c new file mode 100644 index 000000000..a005015d6 --- /dev/null +++ b/packages/runtime/src/scr_util_callable.c @@ -0,0 +1,894 @@ +/* node:util's function helpers over the checked-dynamic tree: inherits, + * deprecate, callbackify and debuglog. The returned wrappers are native + * dyn functions whose closures hold the wrapped values, so they keep + * Node's receiver forwarding, own properties and prototype links through + * every boxing of the same function. The compiled call site converts a + * wrapper back to the program's static function type when it has one. + * + * Warnings follow Node's emitWarning: deprecations are built at the call + * 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. */ +#include "scr_runtime.h" + +#include +#include +#include + +static ScrBox *uc_box(ScrDyn *value /* moves in */) { + ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(box, value); + return box; +} + +static ScrDyn *uc_cap(ScrClosure *closure, size_t index) { + return scr_box_get_ref(closure->caps[index]); /* +1 */ +} + +static void uc_set_cap(ScrClosure *closure, size_t index, ScrDyn *value /* moves in */) { + scr_box_set_ref(closure->caps[index], value); +} + +static bool uc_nullish(const ScrDyn *value) { + return value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL; +} + +static bool uc_object(const ScrDyn *value) { + switch (value->kind) { + case SCR_DYN_OBJ: + case SCR_DYN_ARR: + case SCR_DYN_FUNC: + case SCR_DYN_BYTES: + case SCR_DYN_HANDLE: + case SCR_DYN_PROMISE: + case SCR_DYN_TYPED_REF: + case SCR_DYN_PROXY: + return true; + default: + return false; + } +} + +static void uc_type_error(const char *message) { + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); +} + +static void uc_fence(const char *message) { + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); +} + +/* `value[key]`: own and inherited members of object kinds, undefined for + * primitives (whose prototypes carry none of the keys read here). +1, or + * NULL with an exception pending. */ +static ScrDyn *uc_get(const ScrDyn *value, const char *key) { + size_t len = strlen(key); + ScrDyn *result = NULL; + if (value->kind == SCR_DYN_FUNC) { + result = scr_dyn_fn_get(value, key, len); + } else if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { + scr_dyn_release(view); + return NULL; + } + result = uc_get(view, key); + scr_dyn_release(view); + return result; + } else if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_PROXY) { + result = scr_dyn_obj_read(value, key, len); + } else { + return scr_dyn_retain(scr_dyn_undefined()); + } + if (!result && !scr_exc_pending()) result = scr_dyn_retain(scr_dyn_undefined()); + return result; +} + +/* V8's rendering of a value inside a TypeError message: primitives as + * strings, objects as #. */ +static ScrStr *uc_v8_value(const ScrDyn *value) { + if (value->kind == SCR_DYN_SYMBOL) return scr_insp_dyn((ScrDyn *)value, 0, 0); + if (uc_object(value)) return scr_str_new("#", 9); + ScrStr *text = scr_dyn_string_coerce_js(value); + return text ? text : scr_str_new("", 0); +} + +static void uc_v8_value_error(const char *prefix, const ScrDyn *value, const char *suffix) { + ScrStr *text = uc_v8_value(value); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, prefix); + scr_jb_put_str(&b, text); + scr_jb_puts(&b, suffix); + scr_str_release(text); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); +} + +/* Reflect.apply(fn, receiver, args) — the receiver is bound for the call's + * synchronous extent, as for every dyn method call. +1 or NULL. */ +static ScrDyn *uc_apply(ScrDyn *fn, ScrDyn *receiver, ScrDyn *const *args, size_t argc) { + if (!scr_dyn_is_callable(fn)) { + const char *kind = uc_object(fn) ? "an object" : fn->kind == SCR_DYN_NUM ? "a number" + : fn->kind == SCR_DYN_STR ? "a string" : fn->kind == SCR_DYN_BOOL ? "a boolean" + : fn->kind == SCR_DYN_NULL ? "an object" : fn->kind == SCR_DYN_SYMBOL ? "a symbol" + : fn->kind == SCR_DYN_BIGINT ? "a bigint" : "a undefined"; + ScrStr *text = fn->kind == SCR_DYN_NULL ? scr_str_new("null", 4) : uc_v8_value(fn); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Function.prototype.apply was called on "); + scr_jb_put_str(&b, text); + scr_jb_puts(&b, ", which is "); + scr_jb_puts(&b, kind); + scr_jb_puts(&b, " and not a function"); + scr_str_release(text); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; + } + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(fn, args, argc, "function"); + scr_dyn_this_pop(); + return result; +} + +/* Object.defineProperty(target, key, descriptor) over a fresh descriptor; + * the descriptor moves in. False with an exception pending on failure. */ +static bool uc_define(ScrDyn *target, const char *key, ScrDyn *descriptor) { + ScrStr *name = scr_str_new(key, strlen(key)); + ScrDyn *property = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *defined = scr_dyn_define_property(target, property, descriptor); + scr_dyn_release(property); + scr_dyn_release(descriptor); + if (!defined) return false; + scr_dyn_release(defined); + return true; +} + +/* ── util.inherits ───────────────────────────────────────────────────── + * Node 24's lib/util.js: nullish arguments and an undefined + * superCtor.prototype throw ERR_INVALID_ARG_TYPE; then super_ becomes a + * writable, configurable, non-enumerable own property and + * ctor.prototype's prototype becomes superCtor.prototype. */ +void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) { + if (uc_nullish(ctor)) { + scr_dyn_arg_type_fail("ctor", "of type function", ctor); + return; + } + if (uc_nullish(super_ctor)) { + scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor); + return; + } + if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) || + (super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) { + uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends"); + return; + } + ScrDyn *super_prototype = uc_get(super_ctor, "prototype"); + if (!super_prototype) return; + if (super_prototype->kind == SCR_DYN_UNDEF) { + scr_dyn_prop_type_fail("superCtor.prototype", "of type object", super_prototype); + scr_dyn_release(super_prototype); + return; + } + if (!uc_object(ctor)) { + uc_type_error("Object.defineProperty called on non-object"); + scr_dyn_release(super_prototype); + return; + } + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(super_ctor)); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + if (!uc_define(ctor, "super_", descriptor)) { + scr_dyn_release(super_prototype); + return; + } + ScrDyn *prototype = uc_get(ctor, "prototype"); + if (prototype) { + if (uc_nullish(prototype)) { + uc_type_error("Object.setPrototypeOf called on null or undefined"); + } else if (!uc_object(super_prototype) && super_prototype->kind != SCR_DYN_NULL) { + uc_v8_value_error("Object prototype may only be an Object or null: ", super_prototype, ""); + } else if (uc_object(prototype)) { + scr_dyn_release(scr_dyn_set_prototype(prototype, super_prototype)); + } + scr_dyn_release(prototype); + } + scr_dyn_release(super_prototype); +} + +/* ── util.deprecate ──────────────────────────────────────────────────── + * The wrapper warns once (and once per code across wrappers) before each + * call reaches fn with the call's receiver and arguments; construction + * reaches fn with the fresh receiver. codesWarned is process-wide, like + * Node's. */ +static SCR_TL ScrDyn *uc_codes_warned = NULL; + +static void uc_codes_warned_teardown(void) { + scr_dyn_release(uc_codes_warned); + uc_codes_warned = NULL; +} + +enum { UC_DEP_FN, UC_DEP_MSG, UC_DEP_CODE, UC_DEP_WARNED, UC_DEP_CAPS }; + +/* process.emitWarning(msg, type, code) as deprecate's emitter calls it: + * a string message builds the warning, an Error is emitted as itself, + * anything else throws Node's ERR_INVALID_ARG_TYPE at the call. */ +static bool uc_emit(ScrDyn *msg, const char *type, const ScrStr *code) { + if (!strcmp(type, "DeprecationWarning") && scr_process_deprecation_flag_on(0)) return true; + ScrDyn *warning = NULL; + if (msg->kind == SCR_DYN_STR) { + warning = scr_warning_new(msg->v.str, type, code); + } else if (scr_dyn_ref_is_error(msg)) { + warning = scr_dyn_retain(msg); + } else { + scr_dyn_arg_type_fail("warning", "of type string or an instance of Error", msg); + return false; + } + scr_warning_emit_deferred(warning); + scr_dyn_release(warning); + return true; +} + +static bool uc_deprecation_warn(ScrClosure *closure) { + if (scr_box_get_f64(closure->caps[UC_DEP_WARNED]) != 0) return true; + scr_box_set_f64(closure->caps[UC_DEP_WARNED], 1); + ScrDyn *msg = uc_cap(closure, UC_DEP_MSG); + ScrDyn *code = uc_cap(closure, UC_DEP_CODE); + bool ok = true; + if (code->kind == SCR_DYN_STR && code->v.str->len == 19 && + memcmp(code->v.str->data, "ExperimentalWarning", 19) == 0) { + ok = uc_emit(msg, "ExperimentalWarning", NULL); + } else if (code->kind == SCR_DYN_STR) { + if (!uc_codes_warned) { + uc_codes_warned = scr_dyn_new_obj_null_proto(); + scr_atexit(uc_codes_warned_teardown); + } + const ScrStr *key = code->v.str; + if (!scr_dyn_obj_get(uc_codes_warned, key->data, key->len)) { + ok = uc_emit(msg, "DeprecationWarning", key); + if (ok) scr_dyn_obj_set(uc_codes_warned, key->data, key->len, scr_dyn_new_bool(true)); + } + } else { + ok = uc_emit(msg, "DeprecationWarning", NULL); + } + scr_dyn_release(code); + scr_dyn_release(msg); + return ok; +} + +static ScrDyn *uc_deprecated_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + if (!scr_process_deprecation_flag_on(0) && !uc_deprecation_warn(closure)) return NULL; + ScrDyn *fn = uc_cap(closure, UC_DEP_FN); + ScrDyn *receiver = scr_dyn_this_get(); + ScrDyn *result = uc_apply(fn, receiver, args, argc); + scr_dyn_release(receiver); + scr_dyn_release(fn); + return result; +} + +ScrDyn *scr_util_deprecate(ScrDyn *fn, ScrDyn *msg, ScrDyn *code, ScrDyn *options) { + /* The `{ modifyPrototype } = {}` parameter binds before the body runs. */ + bool modify_prototype = true; + if (options->kind == SCR_DYN_NULL) { + uc_type_error("Cannot destructure property 'modifyPrototype' of " + "'(intermediate value)(intermediate value)(intermediate value)' as it is null."); + return NULL; + } + if (uc_object(options)) { + ScrDyn *modify = uc_get(options, "modifyPrototype"); + if (!modify) return NULL; + if (modify->kind != SCR_DYN_UNDEF) modify_prototype = scr_dyn_truthy(modify); + scr_dyn_release(modify); + } + if (code->kind != SCR_DYN_UNDEF && code->kind != SCR_DYN_STR) { + scr_dyn_arg_type_fail("code", "of type string", code); + return NULL; + } + if (fn->kind == SCR_DYN_FUNC && fn->v.fn.class_obj) { + uc_fence("util.deprecate over class constructors has no lowering; deprecate a factory function instead"); + return NULL; + } + ScrClosure *closure = scr_closure_new(NULL, UC_DEP_CAPS); + closure->function_kind = 4; /* an ordinary function with its own prototype */ + closure->caps[UC_DEP_FN] = uc_box(scr_dyn_retain(fn)); + closure->caps[UC_DEP_MSG] = uc_box(scr_dyn_retain(msg)); + closure->caps[UC_DEP_CODE] = uc_box(scr_dyn_retain(code)); + closure->caps[UC_DEP_WARNED] = scr_box_new(SCR_BOX_F64); + ScrDyn *deprecated = scr_dyn_new_func(closure, uc_deprecated_call, 0, "native:util.deprecate", "deprecated"); + if (!modify_prototype) return deprecated; + /* ObjectSetPrototypeOf(deprecated, fn), then the shared prototype and + * fn's own length descriptor. */ + if (fn->kind != SCR_DYN_NULL && !uc_object(fn)) { + uc_v8_value_error("Object prototype may only be an Object or null: ", fn, ""); + scr_dyn_release(deprecated); + return NULL; + } + ScrDyn *linked = scr_dyn_set_prototype(deprecated, fn); + if (!linked) { + scr_dyn_release(deprecated); + return NULL; + } + scr_dyn_release(linked); + if (fn->kind == SCR_DYN_NULL) { + uc_type_error("Cannot read properties of null (reading 'prototype')"); + scr_dyn_release(deprecated); + return NULL; + } + ScrDyn *prototype = uc_get(fn, "prototype"); + if (!prototype) { + scr_dyn_release(deprecated); + return NULL; + } + if (scr_dyn_truthy(prototype)) { + ScrStr *key = scr_str_new("prototype", 9); + scr_dyn_key_set(deprecated, key, prototype); + scr_str_release(key); + } + scr_dyn_release(prototype); + if (scr_exc_pending()) { + scr_dyn_release(deprecated); + return NULL; + } + ScrStr *length_name = scr_str_new("length", 6); + ScrDyn *length_key = scr_dyn_new_str(length_name); + scr_str_release(length_name); + ScrDyn *length = scr_dyn_get_own_property_descriptor(fn, length_key); + scr_dyn_release(length_key); + if (!length || (length->kind == SCR_DYN_OBJ && !uc_define(deprecated, "length", scr_dyn_retain(length)))) { + scr_dyn_release(length); + scr_dyn_release(deprecated); + return NULL; + } + scr_dyn_release(length); + return deprecated; +} + +/* ── 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 + * next tick, so a throwing callback is an uncaught exception there. A + * falsy rejection reason arrives wrapped in ERR_FALSY_VALUE_REJECTION + * whose `reason` is the original value. */ +/* process.nextTick(cb, null, value) or the rejection's cb(err). */ +static void uc_callback_tick(ScrClosure *closure) { + ScrDyn *cb = uc_cap(closure, 0); + ScrDyn *err = uc_cap(closure, 1); + ScrDyn *value = uc_cap(closure, 2); + ScrDyn *args[2] = {err, value}; + size_t argc = (size_t)scr_box_get_f64(closure->caps[3]); + ScrDyn *result = scr_dyn_call(cb, args, argc, "callback"); + scr_dyn_release(result); + scr_dyn_release(value); + scr_dyn_release(err); + scr_dyn_release(cb); +} + +static void uc_schedule_callback(ScrDyn *cb, ScrDyn *err /* moves in */, ScrDyn *value /* moves in */, + size_t argc) { + ScrClosure *tick = scr_closure_new((void *)uc_callback_tick, 4); + tick->caps[0] = uc_box(scr_dyn_retain(cb)); + tick->caps[1] = uc_box(err); + tick->caps[2] = uc_box(value); + tick->caps[3] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(tick->caps[3], (double)argc); + scr_next_tick(tick); +} + +static ScrDyn *uc_callbackify_fulfilled(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *cb = uc_cap(closure, 0); + uc_schedule_callback(cb, scr_dyn_new_null(), scr_dyn_retain(argc ? args[0] : scr_dyn_undefined()), 2); + scr_dyn_release(cb); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *uc_callbackify_rejected(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *cb = uc_cap(closure, 0); + ScrDyn *reason = argc ? args[0] : scr_dyn_undefined(); + ScrDyn *err; + if (scr_dyn_truthy(reason)) { + err = scr_dyn_retain(reason); + } else { + static const char message[] = "Promise was rejected with falsy value"; + ScrStr *text = scr_str_new(message, sizeof message - 1); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + scr_error_set_code(e, "ERR_FALSY_VALUE_REJECTION"); + err = scr_dyn_from_error(e); + scr_error_release(e); + scr_dyn_obj_set(err, "reason", 6, scr_dyn_retain(reason)); + } + uc_schedule_callback(cb, err, scr_dyn_retain(scr_dyn_undefined()), 1); + scr_dyn_release(cb); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *uc_reaction(ScrDyn *cb, ScrDynThunk thunk) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = uc_box(scr_dyn_retain(cb)); + return scr_dyn_new_func(closure, thunk, 1, "native:util.callbackify", ""); +} + +static void uc_last_argument_fail(const ScrDyn *got) { + char detail[256]; + const char *received = scr_dyn_specific_type(got, detail, sizeof detail); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "The last argument must be of type function. Received "); + scr_jb_puts(&b, received); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_INVALID_ARG_TYPE"); + scr_str_release(message); +} + +static ScrDyn *uc_callbackified_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *maybe_cb = argc ? args[argc - 1] : scr_dyn_undefined(); + if (maybe_cb->kind == SCR_DYN_PROXY && scr_dyn_is_callable(maybe_cb)) { + uc_fence("util.callbackify callbacks that are Proxy values have no lowering"); + return NULL; + } + if (maybe_cb->kind != SCR_DYN_FUNC) { + uc_last_argument_fail(maybe_cb); + return NULL; + } + ScrDyn *receiver = scr_dyn_this_get(); + ScrDyn *bind_args[1] = {receiver}; + ScrDyn *cb = scr_dyn_bind(maybe_cb, bind_args, 1); + ScrDyn *original = uc_cap(closure, 0); + ScrDyn *returned = uc_apply(original, receiver, args, argc ? argc - 1 : 0); + scr_dyn_release(original); + scr_dyn_release(receiver); + if (!returned) { + scr_dyn_release(cb); + return NULL; + } + ScrDyn *on_fulfilled = uc_reaction(cb, uc_callbackify_fulfilled); + ScrDyn *on_rejected = uc_reaction(cb, uc_callbackify_rejected); + scr_dyn_release(cb); + ScrDyn *chained = NULL; + ScrPromise *promise = scr_dyn_promise_of(returned); + if (promise) { + chained = scr_dyn_promise_then(promise, on_fulfilled, on_rejected, NULL); + } else if (uc_nullish(returned)) { + uc_type_error(returned->kind == SCR_DYN_NULL ? "Cannot read properties of null (reading 'then')" + : "Cannot read properties of undefined (reading 'then')"); + } else { + ScrDyn *then = uc_get(returned, "then"); + if (then && scr_dyn_is_callable(then)) { + ScrDyn *reactions[2] = {on_fulfilled, on_rejected}; + chained = uc_apply(then, returned, reactions, 2); + } else if (then) { + uc_type_error("ReflectApply(...).then is not a function"); + } + scr_dyn_release(then); + } + scr_dyn_release(on_rejected); + scr_dyn_release(on_fulfilled); + scr_dyn_release(returned); + if (!chained) return NULL; + scr_dyn_release(chained); + return scr_dyn_retain(scr_dyn_undefined()); +} + +ScrDyn *scr_util_callbackify(ScrDyn *original) { + if (!scr_dyn_is_callable(original)) { + scr_dyn_arg_type_fail("original", "of type function", original); + return NULL; + } + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->function_kind = 4; /* an ordinary function with its own prototype */ + closure->caps[0] = uc_box(scr_dyn_retain(original)); + ScrDyn *callbackified = + scr_dyn_new_func(closure, uc_callbackified_call, 0, "native:util.callbackify", "callbackified"); + /* original's own descriptors, with a numeric length one longer and a + * string name suffixed "Callbackified". */ + ScrDyn *descriptors = scr_dyn_get_own_property_descriptors(original); + if (!descriptors) { + scr_dyn_release(callbackified); + return NULL; + } + if (descriptors->kind == SCR_DYN_OBJ) { + ScrDyn *length = scr_dyn_obj_get(descriptors, "length", 6); + const ScrDyn *length_value = length && length->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(length, "value", 5) : NULL; + if (length_value && length_value->kind == SCR_DYN_NUM) + scr_dyn_obj_set(length, "value", 5, scr_dyn_new_num(length_value->v.num + 1)); + ScrDyn *name = scr_dyn_obj_get(descriptors, "name", 4); + const ScrDyn *name_value = name && name->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(name, "value", 5) : NULL; + if (name_value && name_value->kind == SCR_DYN_STR) { + ScrStr *suffix = scr_str_new("Callbackified", 13); + ScrStr *joined = scr_str_concat(name_value->v.str, suffix); + scr_str_release(suffix); + scr_dyn_obj_set(name, "value", 5, scr_dyn_new_str(joined)); + scr_str_release(joined); + } + } + ScrDyn *defined = scr_dyn_define_props(callbackified, descriptors); + scr_dyn_release(descriptors); + if (!defined) { + scr_dyn_release(callbackified); + return NULL; + } + scr_dyn_release(defined); + 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_dyn_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 + * (a writer for enabled sections, a no-op otherwise), hands it to the + * callback with its own `enabled` getter, and forwards every later call + * while the section is enabled. */ +enum { UC_LOG_SET, UC_LOG_CB, UC_LOG_ENABLED, UC_LOG_DEBUG, UC_LOG_TESTED, UC_LOG_CAPS }; + +static SCR_TL ScrDyn *uc_debug_impls = NULL; +static SCR_TL ScrRegex *uc_debug_pattern = NULL; +static SCR_TL bool uc_debug_pattern_ready = false; + +static void uc_debug_teardown(void) { + scr_dyn_release(uc_debug_impls); + uc_debug_impls = NULL; + scr_regex_release(uc_debug_pattern); + uc_debug_pattern = NULL; + uc_debug_pattern_ready = false; +} + +static void uc_debug_register(void) { + static SCR_TL bool registered = false; + if (registered) return; + registered = true; + scr_atexit(uc_debug_teardown); +} + +/* initializeDebugEnv: ^name$ alternatives over the comma list, with '*' + * as a wildcard and the other regexp syntax escaped, case-insensitive. */ +static bool uc_debug_test(const ScrStr *section) { + if (!uc_debug_pattern_ready) { + uc_debug_pattern_ready = true; + uc_debug_register(); + const char *env = scr_node_debug_initial(); + if (env && *env) { + ScrJsonBuf pattern; + scr_jb_init(&pattern); + scr_jb_putc(&pattern, '^'); + for (const char *p = env; *p; p++) { + if (strchr("|\\{}()[]^$+?.", *p)) { + scr_jb_putc(&pattern, '\\'); + scr_jb_putc(&pattern, *p); + } else if (*p == '*') { + scr_jb_puts(&pattern, ".*"); + } else if (*p == ',') { + scr_jb_puts(&pattern, "$|^"); + } else { + scr_jb_putc(&pattern, *p); + } + } + scr_jb_putc(&pattern, '$'); + ScrStr *source = scr_jb_finish(&pattern); + ScrStr *flags = scr_str_new("i", 1); + uc_debug_pattern = scr_regex_new(source, flags); + scr_str_release(flags); + scr_str_release(source); + if (scr_exc_pending()) return false; + } + } + return uc_debug_pattern != NULL && scr_regex_test(uc_debug_pattern, (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); + if (!message) return NULL; + ScrDyn *set = uc_cap(closure, 0); + char pid[64]; + long id = (long)scr_process_pid(); + if (colors) snprintf(pid, sizeof pid, "\x1b[33m%ld\x1b[39m", id); + else snprintf(pid, sizeof pid, "%ld", id); + ScrJsonBuf line; + scr_jb_init(&line); + scr_jb_put_str(&line, set->v.str); + scr_jb_putc(&line, ' '); + scr_jb_puts(&line, pid); + scr_jb_puts(&line, ": "); + scr_jb_put_str(&line, message); + scr_jb_putc(&line, '\n'); + ScrStr *text = scr_jb_finish(&line); + scr_process_stderr_write(text); + scr_str_release(text); + scr_str_release(message); + scr_dyn_release(set); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *uc_debug_noop(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_dyn_retain(scr_dyn_undefined()); +} + +/* debuglogImpl: one implementation per upper-cased section. */ +static ScrDyn *uc_debug_impl(bool enabled, ScrDyn *set) { + uc_debug_register(); + if (!uc_debug_impls) uc_debug_impls = scr_dyn_new_obj_null_proto(); + const ScrStr *key = set->v.str; + ScrDyn *impl = scr_dyn_obj_get(uc_debug_impls, key->data, key->len); + if (impl) return scr_dyn_retain(impl); + if (enabled) { + if ((key->len == 4 && !memcmp(key->data, "HTTP", 4)) || (key->len == 5 && !memcmp(key->data, "HTTP2", 5))) { + ScrJsonBuf warning; + scr_jb_init(&warning); + scr_jb_puts(&warning, "Setting the NODE_DEBUG environment variable to '"); + scr_jb_puts(&warning, key->len == 4 ? "http" : "http2"); + scr_jb_puts(&warning, "' can expose sensitive data (such as passwords, tokens and authentication headers) in the resulting log."); + ScrStr *text = scr_jb_finish(&warning); + scr_process_emit_warning_text(text); + scr_str_release(text); + } + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->function_kind = 4; + closure->caps[0] = uc_box(scr_dyn_retain(set)); + impl = scr_dyn_new_func(closure, uc_debug_write, 0, "native:util.debuglog", "debug"); + } else { + impl = scr_dyn_new_func(scr_closure_new(NULL, 0), uc_debug_noop, 0, "native:util.debuglog", "noop"); + } + scr_dyn_obj_set(uc_debug_impls, key->data, key->len, scr_dyn_retain(impl)); + return impl; +} + +/* init(): upper-case the section and test it. */ +static bool uc_debug_init(ScrClosure *closure) { + ScrDyn *set = uc_cap(closure, UC_LOG_SET); + if (uc_nullish(set)) { + scr_dyn_release(set); + uc_type_error("String.prototype.toUpperCase called on null or undefined"); + return false; + } + ScrStr *text = scr_dyn_string_coerce_js(set); + scr_dyn_release(set); + if (!text) return false; + ScrStr *upper = scr_str_to_upper(text); + scr_str_release(text); + uc_set_cap(closure, UC_LOG_SET, scr_dyn_new_str(upper)); + bool enabled = uc_debug_test(upper); + scr_str_release(upper); + if (scr_exc_pending()) return false; + uc_set_cap(closure, UC_LOG_ENABLED, scr_dyn_new_bool(enabled)); + return true; +} + +static ScrDyn *uc_enabled_getter(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; (void)argc; + return uc_cap(closure, 0); +} + +static ScrDyn *uc_enabled_accessor(ScrBox *enabled) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_retain(enabled); + return scr_dyn_new_func(closure, uc_enabled_getter, 0, "native:util.debuglog", "get"); +} + +static bool uc_define_enabled(ScrDyn *target, ScrDyn *getter /* moves in */) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "get", 3, getter); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true)); + return uc_define(target, "enabled", descriptor); +} + +static ScrDyn *uc_logger_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *enabled = uc_cap(closure, UC_LOG_ENABLED); + bool disabled = enabled->kind == SCR_DYN_BOOL && !enabled->v.b; + scr_dyn_release(enabled); + if (disabled) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *debug = uc_cap(closure, UC_LOG_DEBUG); + if (debug->kind == SCR_DYN_UNDEF) { + /* The first call: select the implementation, then the callback. */ + if (!uc_debug_init(closure)) return NULL; + enabled = uc_cap(closure, UC_LOG_ENABLED); + ScrDyn *set = uc_cap(closure, UC_LOG_SET); + debug = uc_debug_impl(enabled->v.b, set); + scr_dyn_release(set); + scr_dyn_release(enabled); + uc_set_cap(closure, UC_LOG_DEBUG, scr_dyn_retain(debug)); + ScrDyn *cb = uc_cap(closure, UC_LOG_CB); + if (cb->kind == SCR_DYN_FUNC) { + bool ok = uc_define_enabled(debug, uc_enabled_accessor(closure->caps[UC_LOG_ENABLED])); + ScrDyn *result = ok ? scr_dyn_call(cb, &debug, 1, "callback") : NULL; + scr_dyn_release(result); + if (!result) { + scr_dyn_release(cb); + scr_dyn_release(debug); + return NULL; + } + } + scr_dyn_release(cb); + } + ScrDyn *result = scr_dyn_call(debug, args, argc, "debug"); + scr_dyn_release(debug); + return result; +} + +static ScrDyn *uc_logger_enabled(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; (void)argc; + if (scr_box_get_f64(closure->caps[UC_LOG_TESTED]) == 0) { + if (!uc_debug_init(closure)) return NULL; + scr_box_set_f64(closure->caps[UC_LOG_TESTED], 1); + } + return uc_cap(closure, UC_LOG_ENABLED); +} + +ScrDyn *scr_util_debuglog(ScrDyn *section, ScrDyn *callback) { + ScrClosure *closure = scr_closure_new(NULL, UC_LOG_CAPS); + closure->caps[UC_LOG_SET] = uc_box(scr_dyn_retain(section)); + closure->caps[UC_LOG_CB] = uc_box(scr_dyn_retain(callback)); + closure->caps[UC_LOG_ENABLED] = uc_box(scr_dyn_retain(scr_dyn_undefined())); + closure->caps[UC_LOG_DEBUG] = uc_box(scr_dyn_retain(scr_dyn_undefined())); + closure->caps[UC_LOG_TESTED] = scr_box_new(SCR_BOX_F64); + /* The `enabled` getter shares the logger's state boxes. */ + ScrClosure *getter = scr_closure_new(NULL, UC_LOG_CAPS); + for (size_t i = 0; i < UC_LOG_CAPS; i++) getter->caps[i] = scr_box_retain(closure->caps[i]); + ScrDyn *logger = scr_dyn_new_func(closure, uc_logger_call, 0, "native:util.debuglog", "logger"); + ScrDyn *get = scr_dyn_new_func(getter, uc_logger_enabled, 0, "native:util.debuglog", "get"); + if (!uc_define_enabled(logger, get)) { + scr_dyn_release(logger); + return NULL; + } + return logger; +} diff --git a/packages/runtime/src/scr_util_style.c b/packages/runtime/src/scr_util_style.c index 31f8f6e14..78e3a9b5f 100644 --- a/packages/runtime/src/scr_util_style.c +++ b/packages/runtime/src/scr_util_style.c @@ -173,7 +173,7 @@ done: /* styleText only needs the color/no-color distinction, rather than the * full 4/8/24-bit depth. Follow the ordering of Node 24 internal/tty. */ -static bool util_style_terminal_colors(void) { +bool scr_util_terminal_colors(void) { const char *force = scr_getenv("FORCE_COLOR"); if (force) { bool enabled = !strcmp(force, "") || !strcmp(force, "true") || @@ -294,8 +294,8 @@ ScrStr *scr_util_style_text(const ScrDyn *format, const ScrDyn *text, const ScrD scr_dyn_arg_type_fail("stream", "an instance of ReadableStream, WritableStream, or Stream", stream); goto done; } } - colorize = scr_getenv("FORCE_COLOR") ? util_style_terminal_colors() : - scr_process_is_tty(fd) && util_style_terminal_colors(); + colorize = scr_getenv("FORCE_COLOR") ? scr_util_terminal_colors() : + scr_process_is_tty(fd) && scr_util_terminal_colors(); } } scr_dyn_release(view); view = util_style_view(format); diff --git a/tests/corpus/2215-process-emit-warning-exit.cjs b/tests/corpus/2215-process-emit-warning-exit.cjs index 5c7c8ccc9..b8ab670ab 100644 --- a/tests/corpus/2215-process-emit-warning-exit.cjs +++ b/tests/corpus/2215-process-emit-warning-exit.cjs @@ -5,9 +5,8 @@ // listeners receive the warning object (name/message/code/detail), the // default stderr report prints "(node:pid) [CODE] Name: message" with // the detail on its own line, wrong kinds throw ERR_INVALID_ARG_TYPE, -// and off() removes by identity. Emission here is SYNCHRONOUS where -// Node defers a tick (SEMANTICS.md) — the emits sit last in the turn so -// the transcripts agree. The default report carries the pid, which can +// and off() removes by identity. Warnings are emitted on the next tick, +// before the immediate that exits. The default report carries the pid, which can // never byte-match — the @exit directive (line 1: the harness reads the // two-line head) keeps stderr out of the comparison (the 1651 precedent). 'use strict'; @@ -38,7 +37,6 @@ process.emitWarning(e, 'IgnoredType'); process.on('exit', () => { console.log(seen.join(' ; ')); }); -// Node fires the deferred warnings before the immediate; the compiled -// runtime already dispatched them synchronously — either way the exit -// listener sees the full transcript. +// The deferred warnings fire before the immediate, so the exit listener +// sees the full transcript. setImmediate(() => process.exit(1)); diff --git a/tests/corpus/5185-util-inherits.cjs b/tests/corpus/5185-util-inherits.cjs new file mode 100644 index 000000000..764f6be9e --- /dev/null +++ b/tests/corpus/5185-util-inherits.cjs @@ -0,0 +1,81 @@ +// util.inherits over JavaScript constructor functions: the subclass +// prototype chains to the superclass prototype, super_ is a writable, +// configurable, non-enumerable own property, methods added before and +// after the call are both reachable, and the destructured, member-read +// and namespace forms behave alike. Invalid arguments throw Node's +// ERR_INVALID_ARG_TYPE messages, and primitive or plain-object +// constructors fail where Node's own property operations fail. +"use strict"; +const util = require("util"); +const { inherits } = require("util"); +const memberInherits = require("node:util").inherits; + +function Animal(name) { + this.name = name; +} +Animal.prototype.speak = function () { + return this.name + " makes a sound"; +}; +Animal.kingdom = "animalia"; + +function Dog(name) { + Animal.call(this, name); +} +Dog.prototype.early = function () { + return "defined before inherits"; +}; +util.inherits(Dog, Animal); +Dog.prototype.bark = function () { + return this.name + " barks"; +}; + +const rex = new Dog("rex"); +console.log(rex.speak(), "|", rex.bark(), "|", rex.early()); +console.log(Dog.super_ === Animal, Object.getPrototypeOf(Dog.prototype) === Animal.prototype); +console.log(Object.getPrototypeOf(rex) === Dog.prototype, Dog.prototype.constructor === Dog); +console.log(Object.keys(Dog), Dog.kingdom); +const descriptor = Object.getOwnPropertyDescriptor(Dog, "super_"); +console.log(descriptor.value === Animal, descriptor.writable, descriptor.enumerable, descriptor.configurable); +Reflect.set(Dog, "super_", "reassigned"); +console.log(Reflect.get(Dog, "super_")); + +function Cat(name) { + Animal.call(this, name); +} +inherits(Cat, Animal); +function Lion(name) { + Cat.call(this, name); +} +memberInherits(Lion, Cat); +const leo = new Lion("leo"); +console.log(leo.speak(), Lion.super_ === Cat, Object.getPrototypeOf(Object.getPrototypeOf(leo)) === Cat.prototype); +console.log(Object.getPrototypeOf(Cat.prototype) === Animal.prototype); + +const holder = { link: util.inherits }; +function Fish(name) { + Animal.call(this, name); +} +holder.link(Fish, Animal); +console.log(new Fish("nemo").speak()); +console.log(util.inherits.name, util.inherits.length, inherits === util.inherits); + +function attempt(label, ctor, superCtor) { + try { + const result = util.inherits(ctor, superCtor); + console.log(label, "ok", result); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +function Plain() {} +attempt("undefined ctor", undefined, Animal); +attempt("null ctor", null, Animal); +attempt("undefined super", Plain, undefined); +attempt("null super", Plain, null); +attempt("object super", Plain, {}); +attempt("arrow super", Plain, () => 1); +attempt("number super", Plain, 5); +attempt("number ctor", 5, Animal); +attempt("object ctor", {}, Animal); +attempt("prototype object", Plain, { prototype: { shared: true } }); +console.log(Object.getPrototypeOf(Plain.prototype).shared); diff --git a/tests/corpus/5186-util-inherits.ts b/tests/corpus/5186-util-inherits.ts new file mode 100644 index 000000000..4f9534f0d --- /dev/null +++ b/tests/corpus/5186-util-inherits.ts @@ -0,0 +1,56 @@ +// util.inherits from TypeScript: constructor functions typed with `this` +// parameters and constructors held as unknown values relink their +// prototype objects at runtime, super_ names the superclass, and objects +// created from the subclass prototype see methods added to the base. +// Wrong argument kinds throw Node's coded TypeErrors. +import * as util from "node:util"; +import { inherits } from "node:util"; + +interface Named { + name: string; +} + +function Base(this: Named, name: string): void { + this.name = name; +} +function Derived(this: Named, name: string): void { + Base.call(this, name); +} +inherits(Derived, Base); + +const derived: unknown = Derived; +const base: unknown = Base; +console.log(Reflect.get(derived as object, "super_") === Base); +const derivedPrototype: unknown = Reflect.get(derived as object, "prototype"); +const basePrototype: unknown = Reflect.get(base as object, "prototype"); +console.log(Object.getPrototypeOf(derivedPrototype as object) === basePrototype); +Reflect.set(basePrototype as object, "greet", function (this: Named) { + return `hello from ${this.name}`; +}); +const instance: unknown = Object.create(derivedPrototype as object); +Reflect.set(instance as object, "name", "ts"); +const greet: unknown = Reflect.get(instance as object, "greet"); +console.log(typeof greet, Reflect.apply(greet as (this: unknown) => string, instance, [])); +console.log(Object.keys(derived as object), Object.getOwnPropertyNames(derived as object).includes("super_")); + +const registry: { [name: string]: unknown } = {}; +registry["Root"] = function Root(): void {}; +registry["Leaf"] = function Leaf(): void {}; +util.inherits(registry["Leaf"], registry["Root"]); +console.log(Reflect.get(registry["Leaf"] as object, "super_") === registry["Root"]); + +const inputs: [string, unknown, unknown][] = [ + ["missing ctor", undefined, Base], + ["null super", Derived, null], + ["plain object super", Derived, {}], + ["string super", Derived, "Base"], +]; +for (const [label, ctor, superCtor] of inputs) { + try { + inherits(ctor, superCtor); + console.log(label, "ok"); + } catch (error) { + const e = error as Error & { code?: string }; + console.log(label, e.name, e.code, e.message); + } +} diff --git a/tests/corpus/5187-util-deprecate.cjs b/tests/corpus/5187-util-deprecate.cjs new file mode 100644 index 000000000..1ed64cf8c --- /dev/null +++ b/tests/corpus/5187-util-deprecate.cjs @@ -0,0 +1,86 @@ +// util.deprecate: the wrapper warns once, then keeps calling through; a +// code warns once across every wrapper sharing it, while wrappers without +// a code warn independently. Warnings are reported after the turn's own +// output, the default report before the program's 'warning' listener, +// with Node's one-time --trace-deprecation hint. The wrapper forwards +// `this` and arguments, links its prototype chain and `prototype` to the +// wrapped constructor, copies its length, and keeps the wrapped +// function's static members reachable; modifyPrototype: false skips that. +// The "ExperimentalWarning" code reports an ExperimentalWarning, an Error +// message is reported as itself, and argument errors match Node. The +// program re-runs itself and prints the child's output with process ids +// normalized. +"use strict"; +const { spawnSync } = require("child_process"); +const util = require("util"); + +function attempt(label, run) { + try { + const result = run(); + console.log(label, typeof result); + return result; + } catch (e) { + console.log(label, e.name, e.code, e.message); + return undefined; + } +} + +if (process.argv[2] === "child") { + process.on("warning", (w) => console.log("listener:", w.name, w.code, w.message)); + const add = util.deprecate(function add(a, b) { + return a + b; + }, "add() is deprecated", "DEP_CORPUS_1"); + console.log("add", add(1, 2), add(3, 4), add.name, add.length); + const sameCode = util.deprecate(() => "same code", "never reported", "DEP_CORPUS_1"); + console.log(sameCode()); + const uncoded = util.deprecate(() => "uncoded", "uncoded() is deprecated"); + console.log(uncoded(), uncoded()); + const uncodedTwin = util.deprecate(() => "twin", "uncoded() is deprecated"); + console.log(uncodedTwin()); + const holder = { + factor: 9, + scaled: util.deprecate(function (n) { + return n * this.factor; + }, "scaled() is deprecated", "DEP_CORPUS_2"), + }; + console.log("this", holder.scaled(2)); + + function Point(x, y) { + this.x = x; + this.y = y; + } + Point.prototype.sum = function () { + return this.x + this.y; + }; + Point.origin = "origin"; + const LegacyPoint = util.deprecate(Point, "Point is deprecated", "DEP_CORPUS_3"); + const p = new LegacyPoint(2, 5); + console.log(p.sum(), Object.getPrototypeOf(p) === Point.prototype, LegacyPoint.prototype === Point.prototype); + console.log(LegacyPoint.origin, LegacyPoint.length, LegacyPoint.name, Object.getPrototypeOf(LegacyPoint) === Point); + + const unlinked = util.deprecate(function unlinked(a, b, c) { + return a + b + c; + }, "unlinked() is deprecated", undefined, { modifyPrototype: false }); + console.log("unlinked", unlinked(1, 2, 3), unlinked.length); + + const experimental = util.deprecate(() => "experimental", "an experimental helper", "ExperimentalWarning"); + console.log(experimental(), experimental()); + const errorMessage = util.deprecate(() => "error message", new TypeError("custom warning type"), "DEP_CORPUS_4"); + console.log(errorMessage()); + + attempt("null options", () => util.deprecate(() => 1, "m", undefined, null)); + attempt("numeric code", () => util.deprecate(() => 1, "m", 5)); + attempt("string fn", () => util.deprecate("abc", "m")); + attempt("undefined fn", () => util.deprecate(undefined, "m")); + attempt("null fn", () => util.deprecate(null, "m")); + attempt("object fn call", () => util.deprecate({}, "object fn is deprecated")()); + attempt("missing message call", () => util.deprecate(() => 1)()); + attempt("numeric message call", () => util.deprecate(() => 1, 42, "DEP_CORPUS_5")()); + console.log("end of turn"); + process.nextTick(() => console.log("tick queued after the calls")); +} else { + const child = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/5188-util-deprecate-flags.cjs b/tests/corpus/5188-util-deprecate-flags.cjs new file mode 100644 index 000000000..66d4c5026 --- /dev/null +++ b/tests/corpus/5188-util-deprecate-flags.cjs @@ -0,0 +1,43 @@ +// process.noDeprecation and process.throwDeprecation as util.deprecate +// and process.emitWarning observe them: both read undefined until +// assigned and keep the assigned value; while noDeprecation is truthy a +// deprecated call neither warns nor uses up its one warning, and a +// DeprecationWarning emitted directly is dropped; while throwDeprecation +// is truthy the warning is thrown from its own tick (reaching +// 'uncaughtException') instead of being reported. Other warning types +// are unaffected, and every warning is emitted after the turn's own +// output. The program re-runs itself and prints the child's output with +// process ids normalized. +"use strict"; +const { spawnSync } = require("child_process"); +const util = require("util"); + +if (process.argv[2] === "child") { + console.log("initial", process.noDeprecation, process.throwDeprecation); + process.on("warning", (w) => console.log("listener:", w.name, w.code, w.message)); + const first = util.deprecate(() => "first", "first() is deprecated"); + process.noDeprecation = true; + console.log("silenced", process.noDeprecation, first(), first()); + process.emitWarning("direct deprecation while silenced", "DeprecationWarning", "DEP_FLAG_1"); + process.emitWarning("ordinary warning while silenced"); + process.noDeprecation = 0; + console.log("restored", process.noDeprecation, first()); + const second = util.deprecate(() => "second", "second() is deprecated", "DEP_FLAG_2"); + console.log(second()); + process.emitWarning("direct deprecation", "DeprecationWarning", "DEP_FLAG_3"); + console.error("stderr written during the turn"); + process.on("uncaughtException", (e) => { + console.log("uncaught:", e.name, e.code, e.message, e instanceof Error); + }); + process.throwDeprecation = "yes"; + const third = util.deprecate(() => "third", "third() is deprecated", "DEP_FLAG_4"); + console.log(third(), process.throwDeprecation); + process.emitWarning("thrown direct deprecation", { type: "DeprecationWarning", code: "DEP_FLAG_5" }); + process.emitWarning("ordinary warning while throwing", "CustomWarning"); + console.log("end of turn"); +} else { + const child = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/5189-util-deprecate.ts b/tests/corpus/5189-util-deprecate.ts new file mode 100644 index 000000000..a9d48e774 --- /dev/null +++ b/tests/corpus/5189-util-deprecate.ts @@ -0,0 +1,41 @@ +// util.deprecate from TypeScript: the wrapper keeps the wrapped +// function's static type, so typed arguments and results pass through, +// it is callable from a stored value, and it warns once per code and once +// per uncoded wrapper on the next tick, report first and then the +// program's typed 'warning' listener. process.noDeprecation is a typed +// boolean property. The program re-runs itself and prints the child's +// output with process ids normalized. +import { spawnSync } from "node:child_process"; +import * as util from "node:util"; +import { deprecate } from "node:util"; + +if (process.argv[2] === "child") { + process.on("warning", (warning: Error) => { + console.log(`listener: ${warning.name}: ${warning.message}`); + }); + const format = deprecate( + (label: string, count: number): string => `${label}=${count}`, + "format() is deprecated", + "DEP_TS_1", + ); + const formatted: string[] = []; + for (let i = 0; i < 3; i++) formatted.push(format("item", i)); + console.log(formatted.join(", ")); + const total = util.deprecate((values: number[]): number => values.reduce((a, b) => a + b, 0), "total() is deprecated"); + const stored: (values: number[]) => number = total; + console.log(stored([1, 2, 3]), total([10])); + const sameCode = util.deprecate((): boolean => true, "never reported", "DEP_TS_1"); + console.log(sameCode()); + const maybe = deprecate((value?: string): string => value ?? "none", "maybe() is deprecated", "DEP_TS_2"); + console.log(maybe(), maybe("given")); + process.noDeprecation = true; + const quiet = deprecate((): string => "quiet", "quiet() is deprecated", "DEP_TS_3"); + console.log(quiet(), process.noDeprecation === true); + process.noDeprecation = false; + console.log("end of turn"); +} else { + const child = spawnSync(process.execPath, [process.argv[1]!, "child"], { encoding: "utf8" }); + console.log(`status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/5190-util-callbackify.cjs b/tests/corpus/5190-util-callbackify.cjs new file mode 100644 index 000000000..f9c75a28d --- /dev/null +++ b/tests/corpus/5190-util-callbackify.cjs @@ -0,0 +1,77 @@ +// util.callbackify: fulfilled values reach the callback as (null, value) +// and rejections as (reason) on a later tick, after the turn's own +// output; falsy reasons arrive wrapped in ERR_FALSY_VALUE_REJECTION with +// the original value on .reason, other reasons as themselves. The wrapper +// forwards `this` to both the original and the callback, takes the +// original's name and length plus one, works over thenables, and throws +// synchronously for a missing callback, a non-promise result, or a +// throwing original. +"use strict"; +const util = require("util"); + +function attempt(label, run) { + try { + run(); + console.log(label, "returned"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const double = util.callbackify(async (x) => x * 2); +double(21, (err, value) => console.log("fulfilled", err, value)); + +const reject = util.callbackify(async (reason) => { + throw reason; +}); +for (const reason of [0, null, "", false, undefined, NaN]) { + reject(reason, (err) => { + console.log("falsy", typeof reason, err.name, err.code, err.message, Object.is(err.reason, reason), Object.keys(err)); + }); +} +reject(5, (err, value) => console.log("truthy", err, value)); +reject(new RangeError("out of range"), (err) => console.log("error", err.name, err.message, err instanceof RangeError)); + +const receiver = { + base: 40, + add: util.callbackify(async function (n) { + return this.base + n; + }), +}; +receiver.add(2, function (err, value) { + console.log("receiver", err, value, this === receiver); +}); + +async function fetchRecord(id, field) { + return `${id}:${field}`; +} +const named = util.callbackify(fetchRecord); +const nameDescriptor = Object.getOwnPropertyDescriptor(named, "name"); +const lengthDescriptor = Object.getOwnPropertyDescriptor(named, "length"); +console.log(named.name, named.length, nameDescriptor.writable, nameDescriptor.configurable, lengthDescriptor.enumerable); +named("r1", "title", (err, value) => console.log("named", err, value)); + +const thenableValue = { + /** @param {(value: string) => void} resolve */ + then(resolve) { + console.log("then called"); + resolve("from thenable"); + }, +}; +util.callbackify(() => thenableValue)((err, value) => console.log("thenable", err, value)); + +const loose = { call: util.callbackify(async () => "unused") }; +attempt("no callback", () => loose.call()); +attempt("number callback", () => loose.call(1)); +attempt("object callback", () => loose.call({})); +attempt("non-promise result", () => util.callbackify(() => 5)(() => {})); +attempt("undefined result", () => util.callbackify(() => {})(() => {})); +attempt("sync throw", () => util.callbackify(() => { + throw new Error("thrown synchronously"); +})(() => {})); +for (const original of [undefined, null, 1, "fn", {}, Object.create(null), Object.assign(Object.create(null), { k: 1 })]) { + attempt("original " + typeof original, () => util.callbackify(original)); +} +console.log(util.callbackify.name, util.callbackify.length); +process.nextTick(() => console.log("tick queued during the turn")); +console.log("end of turn"); diff --git a/tests/corpus/5191-util-callbackify.ts b/tests/corpus/5191-util-callbackify.ts new file mode 100644 index 000000000..b859927a4 --- /dev/null +++ b/tests/corpus/5191-util-callbackify.ts @@ -0,0 +1,47 @@ +// util.callbackify from TypeScript: the overloads type the wrapper from +// the async function's parameters and result, so callbacks receive typed +// results through statically typed slots; a void result calls back with +// the error alone, a rejection with an Error reason passes it through, +// and a rejection with a falsy reason arrives as ERR_FALSY_VALUE_REJECTION +// with the original value on .reason. Callbacks run after the turn's own +// output, in settlement order. +import * as util from "node:util"; +import { callbackify } from "node:util"; + +interface Account { + id: number; + owner: string; +} + +async function loadAccount(id: number, owner: string): Promise { + return { id, owner }; +} +const loadCallback = callbackify(loadAccount); +loadCallback(7, "ada", (err, account) => { + console.log("account", err, account.id, account.owner); +}); + +const sum = util.callbackify(async (values: number[]): Promise => values.reduce((a, b) => a + b, 0)); +sum([1, 2, 3, 4], (err, total) => { + console.log("sum", err, total + 0.5); +}); + +const touch = callbackify(async (): Promise => {}); +touch((err) => { + console.log("void", err); +}); + +const fail = callbackify(async (message: string): Promise => { + throw new TypeError(message); +}); +fail("bad input", (err, value) => { + console.log("rejected", err instanceof TypeError, err?.message, value); +}); + +const falsy = callbackify(async (): Promise => { + throw 0; +}); +falsy((err) => { + console.log("falsy", err?.code, err?.message, err ? Reflect.get(err, "reason") : "missing"); +}); +console.log("end of turn"); diff --git a/tests/corpus/5192-util-debuglog.cjs b/tests/corpus/5192-util-debuglog.cjs new file mode 100644 index 000000000..96d156793 --- /dev/null +++ b/tests/corpus/5192-util-debuglog.cjs @@ -0,0 +1,53 @@ +// util.debuglog: sections match NODE_DEBUG as the process started (comma +// lists, '*' wildcards, case-insensitive, other regexp syntax literal), +// so a later process.env write changes nothing. Enabled loggers write +// "SECTION PID: message" lines to stderr with util.format's substitutions +// and inspected extra arguments; `enabled` is an enumerable getter; the +// callback receives the section's shared implementation (with its own +// `enabled`) on the first call; util.debug is the same function; and an +// enabled http section warns about sensitive data. The program re-runs +// itself under several NODE_DEBUG values and prints the child's output +// with process ids normalized. +"use strict"; +const { spawnSync } = require("child_process"); +const util = require("util"); + +if (process.argv[2] === "child") { + process.env.NODE_DEBUG = "changed-at-runtime"; + const foo = util.debuglog("foo"); + console.log("foo enabled", foo.enabled, Object.keys(foo), foo.name); + console.log(typeof Object.getOwnPropertyDescriptor(foo, "enabled").get); + foo("plain message"); + foo("format %s %d %i %f %j %o %O %c %% done", "str", 42, "17.9px", "2.5e1x", { a: [1, 2] }, 7, { b: { c: { d: 1 } } }, "css"); + foo("extra", 1, { k: "v" }, [1, 2], null, undefined, true, -0); + foo({ first: "object" }, "then string"); + foo("%s and %s", { toString() { return "custom toString"; } }, { plain: true }); + foo("%d %d %d %i", 10n, Symbol("s"), "abc", 2n); + foo("missing %s %s", "only-one"); + foo("100%"); + const barBaz = util.debuglog("bar-baz", (fn) => { + console.log("callback", typeof fn, fn.name, fn.enabled); + }); + barBaz("first %s", "call"); + barBaz("second call"); + const other = util.debuglog("other"); + console.log("other enabled", other.enabled); + other("other message"); + const upper = util.debug("FOO"); + upper("same section through util.debug"); + const quiet = util.debuglog("quiet", (fn) => console.log("quiet callback", fn.name, fn.enabled)); + quiet("quiet message"); + console.log("quiet enabled", quiet.enabled); + const http = util.debuglog("http"); + http("request %s", "/path"); + console.log(util.debug === util.debuglog, util.debuglog.name, util.debuglog.length); + console.log("end of turn"); +} else { + for (const env of ["foo,bar-*,HTTP", "", "fo*,Quiet", "o?her,[x],BAR-BAZ", "OTHER"]) { + process.env.NODE_DEBUG = env; + const child = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`NODE_DEBUG=${JSON.stringify(env)} status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/^([A-Z0-9-]+) \d+: /gm, "$1 : ").replace(/\(node:\d+\)/g, "(node:)")); + } +} diff --git a/tests/corpus/5193-util-debuglog.ts b/tests/corpus/5193-util-debuglog.ts new file mode 100644 index 000000000..da3e9cfe7 --- /dev/null +++ b/tests/corpus/5193-util-debuglog.ts @@ -0,0 +1,45 @@ +// util.debuglog from TypeScript: DebugLogger values are callable with a +// typed format string and arguments, `enabled` reads as a boolean, the +// callback receives the typed DebugLoggerFunction, and loggers stored in +// typed slots and records keep working. The program re-runs itself with +// NODE_DEBUG set and unset and prints the child's output with process +// ids normalized. +import { spawnSync } from "node:child_process"; +import * as util from "node:util"; +import { debuglog } from "node:util"; + +interface Service { + name: string; + log: util.DebugLogger; +} + +if (process.argv[2] === "child") { + const log: util.DebugLogger = debuglog("app"); + const enabled: boolean = log.enabled; + console.log("app enabled", enabled); + log("started with %d workers", 4); + const services: Service[] = [ + { name: "db", log: debuglog("app-db") }, + { name: "cache", log: util.debug("app-cache") }, + ]; + for (const service of services) { + service.log("service %s ready", service.name); + console.log(service.name, service.log.enabled); + } + let seen = ""; + const traced = debuglog("app-trace", (fn: util.DebugLoggerFunction) => { + seen = typeof fn; + fn("from the callback"); + }); + traced("first trace %j", { step: 1 }); + console.log("callback saw", seen); + console.log("end of turn"); +} else { + for (const env of ["app,app-d*,APP-TRACE", ""]) { + process.env["NODE_DEBUG"] = env; + const child = spawnSync(process.execPath, [process.argv[1]!, "child"], { encoding: "utf8" }); + console.log(`NODE_DEBUG=${JSON.stringify(env)} status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/^([A-Z0-9-]+) \d+: /gm, "$1 : ")); + } +} diff --git a/tests/corpus/5194-process-emit-warning-tick.cjs b/tests/corpus/5194-process-emit-warning-tick.cjs new file mode 100644 index 000000000..5da488987 --- /dev/null +++ b/tests/corpus/5194-process-emit-warning-tick.cjs @@ -0,0 +1,37 @@ +// process.emitWarning emits on the next tick: argument errors still throw +// at the call, while the report and the 'warning' listeners run after the +// turn's synchronous output, in emission order and interleaved with other +// ticks the way Node's queue orders them (before promise reactions and +// immediates). Warnings emitted from a tick or a microtask follow the +// same rule, and a DeprecationWarning names --trace-deprecation in the +// one-time hint. The program re-runs itself and prints the child's +// output with process ids normalized. +"use strict"; +const { spawnSync } = require("child_process"); + +if (process.argv[2] === "child") { + process.on("warning", (w) => console.log("listener:", w.name, w.code, w.message, w.detail)); + try { + process.emitWarning(42); + } catch (e) { + console.log("sync throw:", e.code); + } + process.emitWarning("first deprecation", "DeprecationWarning", "DEP_TICK_1"); + console.error("stderr during the turn"); + process.nextTick(() => { + console.log("tick after the first warning"); + process.emitWarning("from a tick", { type: "TickWarning", detail: "with detail" }); + }); + Promise.resolve().then(() => { + console.log("microtask"); + process.emitWarning("from a microtask"); + }); + setImmediate(() => console.log("immediate")); + process.emitWarning("second", "CustomWarning", "CW_2"); + console.log("end of turn"); +} else { + const child = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${child.status}`); + console.log(child.stdout); + console.log(child.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 5d5311c7f..d319c0164 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -89,4 +89,9 @@ async function inspectReadableStream(url: string): Promise { const bracketTee = body["tee"]; } void inspectReadableStream; +/* The one- and zero-argument util.callbackify overloads type the + * callback's error as non-null, but Node calls back with null. */ +import { callbackify } from "node:util"; +const callbackified = callbackify(async (n: number) => n); +void callbackified; // End of the declared-but-not-lowered surface. diff --git a/tests/fixtures/node-types/util-callables.ts b/tests/fixtures/node-types/util-callables.ts new file mode 100644 index 000000000..d30fab451 --- /dev/null +++ b/tests/fixtures/node-types/util-callables.ts @@ -0,0 +1,31 @@ +import * as util from "node:util"; +import { callbackify, debuglog, deprecate, inherits } from "node:util"; + +function Base(this: { label: string }, label: string): void { + this.label = label; +} +function Derived(this: { label: string }, label: string): void { + Base.call(this, label); +} +inherits(Derived, Base); +console.log(Reflect.get(Derived, "super_") === Base); + +process.noDeprecation = true; +const add = deprecate((a: number, b: number): number => a + b, "add() is deprecated", "DEP_TYPES"); +console.log(add(2, 3), process.noDeprecation); + +const square: (n: number, callback: (err: NodeJS.ErrnoException | null, value: number) => void) => void = + callbackify(async (n: number): Promise => n * n); +square(9, (err, value) => { + console.log("square", err, value); +}); +const fail: (callback: (err: NodeJS.ErrnoException | null, value?: number) => void) => void = callbackify(async (): Promise => { + throw new RangeError("typed rejection"); +}); +fail((err, value) => { + console.log("fail", err?.name, err?.message, value); +}); + +const log: util.DebugLogger = debuglog("node-types-fixture"); +log("not written while NODE_DEBUG is unset"); +console.log("enabled", log.enabled, util.debug === util.debuglog); diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 6ffee77e9..4b3c086bb 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -212,4 +212,13 @@ fenced.ts:89:22 - error SC2020: 'ReadableStream.tee' is typed by @types/node but | ^~~~~~~~~~~ 90 | } - hint: this operation is outside the native readable-stream implementation \ No newline at end of file + hint: this operation is outside the native readable-stream implementation + +fenced.ts:95:23 - error SC2020: 'util.callbackify into a callback whose error parameter excludes null' is part of the standard library types but has no scriptc lowering yet + + 94 | import { callbackify } from "node:util"; + 95 | const callbackified = callbackify(async (n: number) => n); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 96 | void callbackified; + + hint: Node calls back with a null error on success; type the binding's callback as (err: NodeJS.ErrnoException | null, ...) => void \ No newline at end of file diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index c3c689c58..a3b9f9dd6 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -499,6 +499,24 @@ test("node-types: text encoding APIs match Node under @types/node", async () => expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: util function helpers match Node under @types/node", async () => { + const outDir = outDirFor("node-util-callables"); + const entry = join(nodeTypesDir, "util-callables.ts"); + const result = await compile(entry, { + outPath: join(outDir, "util-callables"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: declared-but-not-lowered surface fences, naming @types/node", async () => { const outDir = outDirFor("node-fenced"); const result = await compile(join(nodeTypesDir, "fenced.ts"), {