Merge main

This commit is contained in:
Chris Tate
2026-10-18 08:45:06 -05:00
121 changed files with 6143 additions and 559 deletions
+1 -1
View File
@@ -331,7 +331,7 @@ $ scriptc build graph.ts
## ProgramLifetime
`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed.
`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed. On Linux, the instances a worker thread creates are returned with the rest of its memory after it exits, unless the worker shared objects with other threads (see [cost](/docs/threads#cost)).
```ts:tree.ts
import type { ProgramLifetime } from "@scriptc/types";
+2
View File
@@ -234,6 +234,8 @@ Without `@scriptc/threads`, a scriptc executable also shares a message of untype
Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. `SCRIPTC_WORKER_STATS=1` prints, as each worker exits, how long it took to start, to run its script and to exit.
On Linux, a worker that exits without having shared an object it allocated with another thread returns all of its memory, including objects it never freed, such as `ProgramLifetime` instances, once no other worker thread is running. A worker shares objects when, for example, it sends a published value or frozen plain data, which other threads read by reference, or stores a value in an `InternTable`. The memory of a worker that shared stays allocated, as on other platforms. Set `SCR_SA_DISCARD=0` in the environment to keep the memory of every worker.
`publish` visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch.
Threads that add to one shared collection of published values while they run can use an [`InternTable`](/docs/experimental#interntable) from `@scriptc/experimental`.
@@ -1 +1 @@
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
@@ -1 +1 @@
- **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported.
- **EventEmitter:** Literal event names use fixed argument tuples. Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion. Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported. Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
@@ -1 +1,2 @@
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module`, `mock.timers` and mock options throw `SC2020`.
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. As in Node, the global timer functions, `Date.now()`, `new Date()` and `scheduler.wait` are mocked, and other calls through named and namespace imports of `node:timers` and `node:timers/promises` use real timers. As in Node, once a reset gives `scheduler.wait` back, calling it throws `ERR_INVALID_THIS`. While `scheduler.wait` is mocked, calling it with a `signal` option throws an `SC2020` error. A mock timer's handle converts to a negative number, where Node's mock `Timeout` converts to `NaN`. When two trackers have timers enabled, clearing a timer through the tracker that did not create it leaves the timer queued, where Node removes the entry at the same queue position in the clearing tracker.
@@ -1 +1,2 @@
- **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node. Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites. Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node.
- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `<entry file>.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`.
@@ -1,6 +1,6 @@
`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB). The `resourceLimits` heap limits (`maxOldGenerationSizeMb`, `maxYoungGenerationSizeMb`, and `codeRangeSizeMb`), `worker.resourceLimits` while the worker runs, and `resourceLimits` read inside a worker are refused; in the main thread `resourceLimits` is `{}`, and after a worker exits `worker.resourceLimits` is `{}`, as in Node. With the `stdout` or `stderr` option, the worker's writes to that stream reach `worker.stdout` or `worker.stderr`, a Readable that ends after the worker's `'exit'` event; its chunks arrive in order with the worker's other events. Without the option a worker writes to the process's own output, and reading `worker.stdout` or `worker.stderr` is refused. With the `stdin` option, `worker.stdin` is a Writable whose chunks the worker reads from `process.stdin`, in order with the worker's other events, and `process.stdin` ends when `worker.stdin` ends; without it `worker.stdin` is `null` and the worker's `process.stdin` ends without data, as in Node. Runtime paths, `eval`, loaders, and shared environments are unsupported. `setEnvironmentData` and `getEnvironmentData` keep a map per thread with SameValueZero keys; each new Worker starts from a clone of its parent's entries taken by the constructor, which throws `DataCloneError` when an entry cannot be cloned. Worker argument strings containing null bytes are also unsupported.
Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. A worker's uncaught exception reaches its `error` listeners as Node's error serializer sends it: an Error keeps the nearest of `Error` and its six native subclasses (an `AggregateError` arrives as an `Error` named `AggregateError`) along with its name, message, cause, and other data properties and their enumerability, and other thrown values clone as messages do. An unhandled rejection whose reason is not error-like arrives as an `UnhandledPromiseRejection` Error with code `ERR_UNHANDLED_REJECTION` quoting the reason. A thrown value that does not clone, such as a function, arrives as an `ERR_WORKER_UNSERIALIZABLE_ERROR` Error, where Node sends its `util.inspect` string. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. `receiveMessageOnPort(parentPort)` in a worker takes the parent's next message. A port's `onmessage` property and `addEventListener` are unsupported. `BroadcastChannel` delivers each message to every other open channel with the same name, in any thread, as a `'message'` `MessageEvent` through `onmessage` and `addEventListener`; an open channel keeps the process alive until `close()` or `unref()`. A `messageerror` event is never dispatched. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. `markAsUntransferable` marks objects for the current thread, and a marked ArrayBuffer or MessagePort in a transfer list throws `DataCloneError`, as does the ArrayBuffer of a pooled Buffer; marked objects other than ArrayBuffers stay alive until their thread exits. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).
Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `worker[Symbol.asyncDispose]()` terminates the worker and resolves with `undefined` after its `exit` event, so `await using` disposes a Worker at the end of its block. A worker's uncaught exception reaches its `error` listeners as Node's error serializer sends it: an Error keeps the nearest of `Error` and its six native subclasses (an `AggregateError` arrives as an `Error` named `AggregateError`) along with its name, message, cause, and other data properties and their enumerability, and other thrown values clone as messages do. An unhandled rejection whose reason is not error-like arrives as an `UnhandledPromiseRejection` Error with code `ERR_UNHANDLED_REJECTION` quoting the reason. A thrown value that does not clone, such as a function, arrives as an `ERR_WORKER_UNSERIALIZABLE_ERROR` Error, where Node sends its `util.inspect` string. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. `receiveMessageOnPort(parentPort)` in a worker takes the parent's next message. A port's `onmessage` property and `addEventListener` are unsupported. `BroadcastChannel` delivers each message to every other open channel with the same name, in any thread, as a `'message'` `MessageEvent` through `onmessage` and `addEventListener`; an open channel keeps the process alive until `close()` or `unref()`. A `messageerror` event is never dispatched. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. `markAsUntransferable` marks objects for the current thread, and a marked ArrayBuffer or MessagePort in a transfer list throws `DataCloneError`, as does the ArrayBuffer of a pooled Buffer; marked objects other than ArrayBuffers stay alive until their thread exits. `markAsUncloneable` marks a plain object, an error, or a class instance, and `structuredClone` and `postMessage` then throw `DataCloneError` for it, also where it is nested; as in Node, the mark has no effect on primitives, arrays, typed arrays, and ArrayBuffers. A plain object that the compiler copies into a container with a fixed element type, such as an array literal whose elements all have one shape, is a separate object in the copy, which is not marked. Marked objects stay alive until their thread exits. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).
SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. Atomics operations run as sequentially consistent atomic instructions and take no lock. Other reads and writes of shared storage access each element atomically; a copy, fill, or conversion that runs while another thread writes the same storage can observe some of those writes, as in Node. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint.
@@ -1,4 +1,4 @@
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
@@ -3,3 +3,5 @@ Casts from values whose type is only known at runtime validate the value. For ex
Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. `SC6004` reports calls that pass such an instance to a function that casts it.
A Promise converted from `unknown` to a typed result validates its fulfilled value. A mismatch rejects with a `TypeError`. Rejection reasons and reference equality are preserved across conversions.
A non-null assertion (`value!`) checks the value when it extracts an object, array or other non-nullable member from a union. If the value holds `null` or `undefined`, the assertion throws a `TypeError`, even when the result is never used; Node.js continues until a property of the value is read.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "f2365d9fcaf87f1ccf46",
"artifactVersion": "cade1b8e6e0c5c42fcc9",
"rowCount": 3880
}
+42 -42
View File
@@ -569,9 +569,9 @@
"apiEntries": 120,
"static": {
"supported": 0,
"partial": 50,
"partial": 51,
"refused": 0,
"not-implemented": 70,
"not-implemented": 69,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -689,9 +689,9 @@
"apiEntries": 117,
"static": {
"supported": 0,
"partial": 45,
"partial": 53,
"refused": 0,
"not-implemented": 72,
"not-implemented": 64,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -884,9 +884,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
"partial": 43,
"partial": 46,
"refused": 0,
"not-implemented": 23,
"not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -57593,9 +57593,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": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -65550,9 +65550,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -65573,9 +65573,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -65596,9 +65596,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -65619,9 +65619,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -65642,9 +65642,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -65665,9 +65665,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -66401,9 +66401,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -66424,9 +66424,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -83564,9 +83564,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -83955,9 +83955,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -84070,9 +84070,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -7,8 +7,8 @@
"tierItemCount": 3456,
"tiers": {
"static": {
"audit-partial": 1131,
"verify-gap": 2198,
"audit-partial": 1143,
"verify-gap": 2186,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -25009,7 +25009,9 @@
"confidence": "high",
"source": "compiler-dedicated:events.getEventListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
}
}
@@ -25036,7 +25038,9 @@
"confidence": "high",
"source": "compiler-dedicated:events.getMaxListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
}
}
@@ -25091,7 +25095,9 @@
"confidence": "high",
"source": "compiler-dedicated:events.listenerCount",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
}
}
@@ -25143,7 +25149,9 @@
"confidence": "high",
"source": "compiler-dedicated:events.setMaxListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
}
}
@@ -25197,7 +25205,9 @@
"confidence": "high",
"source": "compiler-dedicated:events.defaultMaxListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
}
}
@@ -48430,7 +48440,8 @@
"confidence": "high",
"source": "compiler-feature:module.module.isBuiltin",
"tests": [
"tests/corpus/2932-create-require-program/main.ts"
"tests/corpus/2932-create-require-program/main.ts",
"tests/corpus/5846-events-esm-default-max.ts"
]
}
}
@@ -48527,7 +48538,8 @@
"confidence": "high",
"source": "compiler-feature:module.module.syncBuiltinESMExports",
"tests": [
"tests/corpus/2932-create-require-program/main.ts"
"tests/corpus/2932-create-require-program/main.ts",
"tests/corpus/5846-events-esm-default-max.ts"
]
}
}
@@ -57507,12 +57519,15 @@
"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": "surface-manifest:node-builtin.process.execArgv",
"tests": [
"tests/corpus/990-process-basics.ts",
"tests/corpus/2314-process-introspection.ts"
]
}
}
},
@@ -66679,12 +66694,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.timers.enable",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -66704,12 +66725,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.timers.reset",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -66729,12 +66756,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.timers",
"tests": [
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -66754,12 +66784,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.timers.tick",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -66779,12 +66815,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.timers.runAll",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -66804,12 +66846,18 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.timers.setTime",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -67634,12 +67682,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.context.assert.fileSnapshot",
"tests": [
"tests/fixtures/node-test/cases/snapshot.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -67659,12 +67710,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:test",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:test.context.assert.snapshot",
"tests": [
"tests/fixtures/node-test/cases/snapshot.ts",
"tests/harness/node-test.test.ts"
]
}
}
},
@@ -84573,12 +84627,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:worker_threads",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:worker_threads.worker",
"tests": [
"tests/corpus/5991-worker-async-dispose.mjs"
]
}
}
},
@@ -85052,12 +85108,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:worker_threads",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:worker_threads.worker_threads.markAsUncloneable",
"tests": [
"tests/corpus/5990-worker-uncloneable.mjs"
]
}
}
},
@@ -85185,12 +85243,14 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:worker_threads",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:worker_threads.isInternalThread",
"tests": [
"tests/corpus/5990-worker-uncloneable.mjs"
]
}
}
},
@@ -596,9 +596,9 @@
"apiEntries": 120,
"static": {
"supported": 0,
"partial": 50,
"partial": 51,
"refused": 0,
"not-implemented": 70,
"not-implemented": 69,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -746,9 +746,9 @@
"apiEntries": 117,
"static": {
"supported": 0,
"partial": 45,
"partial": 53,
"refused": 0,
"not-implemented": 72,
"not-implemented": 64,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -941,9 +941,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
"partial": 43,
"partial": 46,
"refused": 0,
"not-implemented": 23,
"not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -41819,6 +41819,8 @@
"tests/corpus/5245-event-target-subclass-signals.ts",
"tests/corpus/5247-event-target-subclass-lifetimes.ts",
"tests/corpus/5248-event-target-subclass-warnings.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs",
"tests/corpus/5885-events-max-listener-helpers.ts"
]
},
@@ -43739,7 +43741,9 @@
"status": "partial",
"evidence": "compiler-dedicated:events.getEventListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
},
"anchorSource": "exact"
@@ -43765,7 +43769,9 @@
"status": "partial",
"evidence": "compiler-dedicated:events.getMaxListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
},
"anchorSource": "exact"
@@ -43841,7 +43847,9 @@
"status": "partial",
"evidence": "compiler-dedicated:events.listenerCount",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
},
"anchorSource": "exact"
@@ -43890,7 +43898,9 @@
"status": "partial",
"evidence": "compiler-dedicated:events.setMaxListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
},
"anchorSource": "exact"
@@ -43942,7 +43952,9 @@
"status": "partial",
"evidence": "compiler-dedicated:events.defaultMaxListeners",
"tests": [
"tests/corpus/5885-events-max-listener-helpers.ts"
"tests/corpus/5885-events-max-listener-helpers.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/corpus/5847-events-cjs-default-max.cjs"
]
},
"anchorSource": "exact"
@@ -69416,6 +69428,7 @@
"tests/corpus/2637-create-require-bare/main.ts",
"tests/corpus/2675-create-require-nested-esm.js",
"tests/corpus/2932-create-require-program/main.ts",
"tests/corpus/5846-events-esm-default-max.ts",
"tests/harness/npm-static.test.ts"
]
},
@@ -69503,7 +69516,8 @@
"status": "partial",
"evidence": "compiler-feature:module.module.isBuiltin",
"tests": [
"tests/corpus/2932-create-require-program/main.ts"
"tests/corpus/2932-create-require-program/main.ts",
"tests/corpus/5846-events-esm-default-max.ts"
]
},
"anchorSource": "exact"
@@ -69593,7 +69607,8 @@
"status": "partial",
"evidence": "compiler-feature:module.module.syncBuiltinESMExports",
"tests": [
"tests/corpus/2932-create-require-program/main.ts"
"tests/corpus/2932-create-require-program/main.ts",
"tests/corpus/5846-events-esm-default-max.ts"
]
},
"anchorSource": "exact"
@@ -80949,8 +80964,12 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "surface-manifest:node-builtin.process.execArgv",
"tests": [
"tests/corpus/990-process-basics.ts",
"tests/corpus/2314-process-introspection.ts"
]
},
"anchorSource": "chapter"
},
@@ -90942,11 +90961,16 @@
"tests/corpus/5515-assert-deep-strict-typed-messages.ts",
"tests/corpus/5810-mock-method-records.ts",
"tests/corpus/5811-mock-tracker-restore.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/fixtures/node-test/cases/ctx-hooks.ts",
"tests/fixtures/node-test/cases/ctx-plan.ts",
"tests/fixtures/node-test/cases/mock-fn.ts",
"tests/fixtures/node-test/cases/mock-method.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/only-nested.ts",
"tests/fixtures/node-test/cases/snapshot.ts",
"tests/fixtures/node-test/cases/suite-variants.ts",
"tests/fixtures/node-test/cases/todo-suites.ts",
"tests/harness/node-test.test.ts"
@@ -92576,8 +92600,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.timers.enable",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -92599,8 +92630,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.timers.reset",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -92622,8 +92660,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.timers",
"tests": [
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -92645,8 +92687,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.timers.tick",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -92714,8 +92763,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.timers.runAll",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -92737,8 +92793,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.timers.setTime",
"tests": [
"tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/mock-timers-date.ts",
"tests/corpus/5813-mock-timers-module.ts",
"tests/corpus/5814-mock-timers-date.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -93559,8 +93622,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.context.assert.fileSnapshot",
"tests": [
"tests/fixtures/node-test/cases/snapshot.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -93582,8 +93649,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:test"
"status": "partial",
"evidence": "compiler-feature:test.context.assert.snapshot",
"tests": [
"tests/fixtures/node-test/cases/snapshot.ts",
"tests/harness/node-test.test.ts"
]
},
"anchorSource": "exact"
},
@@ -113045,6 +113116,8 @@
"tests/corpus/5874-worker-stdio-streams.ts",
"tests/corpus/5875-worker-stdout-pipe.cjs",
"tests/corpus/5879-worker-stdin.ts",
"tests/corpus/5990-worker-uncloneable.mjs",
"tests/corpus/5991-worker-async-dispose.mjs",
"tests/corpus/native-worker-clone-errors.ts",
"tests/corpus/native-worker-commonjs.cjs",
"tests/corpus/native-worker-delayed-messages.ts",
@@ -113936,8 +114009,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:worker_threads"
"status": "partial",
"evidence": "compiler-feature:worker_threads.worker",
"tests": [
"tests/corpus/5991-worker-async-dispose.mjs"
]
},
"anchorSource": "exact"
},
@@ -114394,8 +114470,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:worker_threads"
"status": "partial",
"evidence": "compiler-feature:worker_threads.worker_threads.markAsUncloneable",
"tests": [
"tests/corpus/5990-worker-uncloneable.mjs"
]
},
"anchorSource": "exact"
},
@@ -114519,8 +114598,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:worker_threads"
"status": "partial",
"evidence": "compiler-feature:worker_threads.isInternalThread",
"tests": [
"tests/corpus/5990-worker-uncloneable.mjs"
]
},
"anchorSource": "chapter"
},
+7 -2
View File
@@ -211,6 +211,8 @@
{"chapter": "worker_threads", "symbols": ["worker_threads.getEnvironmentData", "worker_threads.setEnvironmentData"], "status": "partial", "evidence": ["tests/corpus/5803-worker-environment-data.ts", "tests/corpus/5804-worker-environment-data.cjs"]},
{"chapter": "worker_threads", "symbols": ["stdout", "stderr", "stdin"], "status": "partial", "evidence": ["tests/corpus/5874-worker-stdio-streams.ts", "tests/corpus/5875-worker-stdout-pipe.cjs", "tests/corpus/5879-worker-stdin.ts"]},
{"chapter": "worker_threads", "symbols": ["threadName", "worker_threads.markAsUntransferable", "worker_threads.isMarkedAsUntransferable"], "status": "partial", "evidence": ["tests/corpus/5870-worker-thread-names.ts", "tests/corpus/5871-worker-untransferable.cjs"]},
{"chapter": "worker_threads", "symbols": ["worker_threads.markAsUncloneable", "isInternalThread"], "status": "partial", "evidence": ["tests/corpus/5990-worker-uncloneable.mjs"]},
{"chapter": "worker_threads", "signatures": ["`worker[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/5991-worker-async-dispose.mjs"]},
{"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]},
{"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
@@ -309,7 +311,7 @@
{ "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] },
{ "chapter": "modules", "symbols": ["extensions"], "status": "not-implemented" },
{ "chapter": "module", "symbols": ["module.createRequire"], "status": "partial", "evidence": ["tests/corpus/2631-create-require/main.ts", "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2675-create-require-nested-esm.js", "tests/corpus/2932-create-require-program/main.ts", "tests/harness/npm-static.test.ts"] },
{ "chapter": "module", "symbols": ["module.isBuiltin", "module.syncBuiltinESMExports"], "status": "partial", "evidence": ["tests/corpus/2932-create-require-program/main.ts"] },
{ "chapter": "module", "symbols": ["module.isBuiltin", "module.syncBuiltinESMExports"], "status": "partial", "evidence": ["tests/corpus/2932-create-require-program/main.ts", "tests/corpus/5846-events-esm-default-max.ts"] },
{ "chapter": "module", "symbols": ["builtinModules"], "status": "partial", "evidence": ["tests/corpus/2631-create-require/main.ts", "tests/corpus/2637-create-require-bare/main.ts", "tests/corpus/2932-create-require-program/main.ts"] },
{ "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1596-cjs-modules/main.js"] },
{ "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts", "tests/corpus/2933-static-dynamic-import/main.ts", "tests/corpus/2934-static-import-stored.ts", "tests/corpus/2935-module-namespace-values/main.ts"] },
@@ -352,9 +354,12 @@
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`href` Type: {string}", "`pathname` Type: {string}", "`search` Type: {string}", "`hash` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "signatures": ["`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/2946-url-port-hash.ts"] },
{ "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] },
{ "chapter": "test", "parent": "`context.assert`", "symbols": ["context.assert.snapshot", "context.assert.fileSnapshot"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/snapshot.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
@@ -385,7 +390,7 @@
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
{ "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts"] },
{ "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] },
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
+25
View File
@@ -3338,7 +3338,12 @@ declare module "node:test" {
beforeEach(fn: HookFn): void;
afterEach(fn: HookFn): void;
}
interface AssertSnapshotOptions {
readonly serializers?: ReadonlyArray<(value: any) => any> | undefined;
}
interface TestContextAssert {
snapshot(value: any, options?: AssertSnapshotOptions): void;
fileSnapshot(value: any, path: string, options?: AssertSnapshotOptions): void;
ok(value: unknown, message?: string): void;
strictEqual(actual: unknown, expected: unknown, message?: string): void;
notStrictEqual(actual: unknown, expected: unknown, message?: string): void;
@@ -3429,6 +3434,19 @@ declare module "node:test" {
: never;
reset(): void;
restoreAll(): void;
readonly timers: MockTimers;
}
interface MockTimersOptions {
apis: ReadonlyArray<"setInterval" | "setTimeout" | "setImmediate" | "Date">;
now?: number | Date | undefined;
}
interface MockTimers {
enable(options?: MockTimersOptions): void;
setTime(milliseconds?: number): void;
reset(): void;
tick(milliseconds?: number): void;
runAll(): void;
[Symbol.dispose](): void;
}
const mock: MockTracker;
/* Declared for typechecking only — every use fences (no lowering).
@@ -3456,6 +3474,8 @@ declare module "node:test" {
MockFunctionContext,
MockFunctionCall,
MockTracker,
MockTimers,
MockTimersOptions,
};
export default test;
}
@@ -5857,6 +5877,8 @@ declare module "worker_threads" {
readonly stderr: import("stream").Readable;
/* with the stdin option: what the worker reads from process.stdin */
readonly stdin: import("stream").Writable | null;
/* terminate(), resolving with undefined once the worker exited */
[Symbol.asyncDispose](): Promise<void>;
on(event: "online", listener: () => void): this;
on(event: "message", listener: (value: any) => void): this;
on(event: "error", listener: (error: any) => void): this;
@@ -5906,6 +5928,9 @@ declare module "worker_threads" {
export function getEnvironmentData(key: any): any;
/* A marked object cannot be listed in a transfer list. */
export function markAsUntransferable(object: any): void;
/* structuredClone and postMessage throw DataCloneError for a marked
* object, error or class instance */
export function markAsUncloneable(object: any): void;
export function isMarkedAsUntransferable(object: any): boolean;
/* Delivers each message to every other open channel with the name, in
* any thread, as a 'message' MessageEvent. */
@@ -3,6 +3,7 @@ import {
BOOL,
F64,
STRING,
UNDEFINED_T,
VOID,
arrayOf,
type IrExpr,
@@ -23,11 +24,17 @@ const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({
});
const index = ref("i", F64);
function module(name: string, returnType: IrExpr["type"], body: IrStmt[]): IrModule {
function module(
name: string,
returnType: IrExpr["type"],
body: IrStmt[],
unions: IrModule["unions"] = [],
): IrModule {
return {
irVersion: 16,
sourceFile: loc.file,
entry: "__main",
unions,
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
{
@@ -174,6 +181,76 @@ test.each([32, 64] as const)(
},
);
test.each([32, 64] as const)(
"pop removes the last element of a packed array inline and keeps a cold runtime pop (%i-bit)",
(bits) => {
const maybe = { kind: "union", unionId: "maybe" } as const;
const pop: IrExpr = {
kind: "arrIntrinsic",
method: "pop",
receiver: ref("a", nums),
args: [],
type: maybe,
loc,
};
const llvm = emitLlvmModule(
module(
"take",
maybe,
[{ kind: "return", value: pop, loc }],
[{ id: "maybe", arms: [F64, UNDEFINED_T] }],
),
{ pointerBits: bits },
);
const fn = body(llvm, "take");
const size = bits === 32 ? "i32" : "i64";
// An empty array answers undefined without a call.
expect(fn).toMatch(new RegExp(`icmp eq ${size} %t\\d+, 0\\n br i1 %t\\d+, label %remove\\.a`));
expect(fn).toMatch(new RegExp(`sub nuw ${size} %t\\d+, 1`));
expect(fn).toMatch(/store i64 0, ptr /);
expect(fn).toMatch(/store i8 0, ptr /);
expect(fn).toMatch(/phi i64 /);
expect(fn).toMatch(/call zeroext i8 @scr_arr_pop_state\(.*\) cold$/m);
},
);
test("a discarded pop releases the removed element inline", () => {
const words = ref("s", arrayOf(STRING));
const length: IrExpr = {
kind: "arrIntrinsic",
method: "length",
receiver: words,
args: [],
type: F64,
loc,
};
const llvm = emitLlvmModule(
module("drop", VOID, [
{
kind: "arraySetLength",
arr: words,
length: {
kind: "bin",
op: "-",
left: length,
right: { kind: "numLit", value: 1, type: F64, loc },
type: F64,
loc,
},
loc,
pop: true,
},
]),
);
const fn = body(llvm, "drop");
expect(fn).toMatch(/fsub double %t\d+, 1\.0/);
expect(fn).toMatch(/load i64, ptr .*\n(?:.*\n)*? store i64 0, ptr /);
expect(fn).toMatch(/inttoptr i64 %t\d+ to ptr/);
expect(fn).toContain("call void @scr_str_release(");
// Holes, sparse storage, and every other length keep the runtime call.
expect(fn).toContain("call void @scr_arr_set_len(");
});
test.each([32, 64] as const)(
"indices below the hole-free prefix bound skip the state bytes (%i-bit)",
(bits) => {
@@ -508,6 +508,75 @@ export function emitDenseArraySet(
B.startBlock(join);
}
/** The dense half of `pop`. An empty array branches to `empty` (the
* runtime's pop returns a hole state there and changes nothing). When the
* hole-free prefix bound equals the length, the last element is a present
* value in dense storage, and the runtime's pop is exactly: clear its slot
* and state, lower the bound and the length to its index, and move the
* slot out. That block is left open and its slot bits are returned as an
* i64 (a boolean's 0 or 1, zero-extended); every other array branches to
* `slow`, which keeps the runtime call. */
export function emitDenseArrayPop(
host: LlvmEmitterContext,
array: string,
acc: ArrayAccess,
empty: string,
slow: string,
): string {
const B = host.B;
const size = host.sizeType;
const header = (field: number): string => {
const ptr = B.tmp();
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
host.markMemoryPointer(ptr, "array:header");
return ptr;
};
const lenPtr = header(SCR_ARR_FIELD.len),
len = B.tmp(),
none = B.tmp();
B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(`${none} = icmp eq ${size} ${len}, 0`);
const check = B.newLabel("arr.pop.check"),
dense = B.newLabel("arr.pop.dense");
B.condBr(none, empty, check);
B.startBlock(check);
const filledPtr = header(SCR_ARR_FIELD.filled),
filled = B.tmp(),
packed = B.tmp();
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`${packed} = icmp eq ${size} ${sizedFilled(host, filled)}, ${len}`);
B.condBr(packed, dense, slow);
B.startBlock(dense);
const last = B.tmp();
B.line(`${last} = sub nuw ${size} ${len}, 1`);
const dataPtr = header(SCR_ARR_FIELD.data),
data = B.tmp(),
statesPtr = header(SCR_ARR_FIELD.present),
states = B.tmp();
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
const valuePtr = valuePointer(host, { offset: last, data }, acc);
const statePtr = statePointer(host, { offset: last, states });
const slotTy = slotType(acc);
let raw = B.tmp();
B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
if (slotTy === "i8") {
const wide = B.tmp();
B.line(`${wide} = zext i8 ${raw} to i64`);
raw = wide;
}
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
let last32 = last;
if (size === "i64") {
last32 = B.tmp();
B.line(`${last32} = trunc i64 ${last} to i32`);
}
B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
return raw;
}
/** `scr_arr_push_*` (the array takes the value's reference) returning the
* new length as a double. When `len < cap` the runtime's append is exactly
* a dense store at `len` plus `len + 1`: no sparse entry can exist past the
+22 -76
View File
@@ -2418,6 +2418,15 @@ export class LlDyn {
const B = new BlockBuilder();
let sourceAccessor: { name: string; release: string } | null = null;
switch (t.kind) {
case "date": {
// A Date handle holding the same time value; native Dates are
// immutable values, and so is the handle.
host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`);
B.terminate(`ret ptr ${r}`);
break;
}
case "url":
case "searchParams": {
const native = t.kind === "url" ? "url" : "search_params";
@@ -3209,82 +3218,19 @@ export class LlDyn {
this.puts(B, "%b", "[object Object]");
B.br(done);
B.startBlock(lErr);
const en = this.objGetLit(B, "%d", "name");
const em = this.objGetLit(B, "%d", "message");
// ens/ems: the member's ScrStr when it is a string, else null.
const strOf = (m: string, hint: string): string => {
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ptr`);
B.line(`store ptr null, ptr ${slot}`);
const nn = B.tmp();
B.line(`${nn} = icmp ne ptr ${m}, null`);
const lt = B.newLabel(`${hint}.t`);
const lj = B.newLabel(`${hint}.j`);
B.condBr(nn, lt, lj);
B.startBlock(lt);
const k = this.kindOf(B, m);
const isStr = B.tmp();
B.line(`${isStr} = icmp eq i32 ${k}, ${DYN_KIND.STR}`);
const ls = B.newLabel(`${hint}.s`);
B.condBr(isStr, ls, lj);
B.startBlock(ls);
const sv = this.payloadOf(B, m, "ptr");
B.line(`store ptr ${sv}, ptr ${slot}`);
B.br(lj);
B.startBlock(lj);
const out = B.tmp();
B.line(`${out} = load ptr, ptr ${slot}`);
return out;
};
const ens = strOf(en, "ds.en");
const ems = strOf(em, "ds.em");
const putIf = (s: string, hint: string): void => {
const nn = B.tmp();
B.line(`${nn} = icmp ne ptr ${s}, null`);
const lt = B.newLabel(`${hint}.t`);
const lj = B.newLabel(`${hint}.j`);
B.condBr(nn, lt, lj);
B.startBlock(lt);
this.putScrStr(B, "%b", s);
B.br(lj);
B.startBlock(lj);
};
putIf(ens, "ds.pn");
{
// if (ens && ens->len && ems && ems->len) ": "
const lenNz = (s: string, hint: string): string => {
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca i1`);
B.line(`store i1 false, ptr ${slot}`);
const nn = B.tmp();
B.line(`${nn} = icmp ne ptr ${s}, null`);
const lt = B.newLabel(`${hint}.t`);
const lj = B.newLabel(`${hint}.j`);
B.condBr(nn, lt, lj);
B.startBlock(lt);
const { len } = this.strParts(B, s);
const nz = B.tmp();
B.line(`${nz} = icmp ne ${this.S} ${len}, 0`);
B.line(`store i1 ${nz}, ptr ${slot}`);
B.br(lj);
B.startBlock(lj);
const out = B.tmp();
B.line(`${out} = load i1, ptr ${slot}`);
return out;
};
const a = lenNz(ens, "ds.ln");
const b2 = lenNz(ems, "ds.lm");
const both = B.tmp();
B.line(`${both} = and i1 ${a}, ${b2}`);
const lt = B.newLabel("ds.cl");
const lj = B.newLabel("ds.cj");
B.condBr(both, lt, lj);
B.startBlock(lt);
this.puts(B, "%b", ": ");
B.br(lj);
B.startBlock(lj);
}
putIf(ems, "ds.pm");
// Error.prototype.toString, reading name and message through the
// prototype. Null leaves the exception its conversion threw pending.
host.declare(`declare ptr @scr_dyn_error_string(ptr)`);
host.declare(`declare void @scr_str_release(ptr)`);
const text = B.tmp();
B.line(`${text} = call ptr @scr_dyn_error_string(ptr %d)`);
const written = B.tmp();
B.line(`${written} = icmp ne ptr ${text}, null`);
const lPut = B.newLabel("ds.ep");
B.condBr(written, lPut, done);
B.startBlock(lPut);
this.putScrStr(B, "%b", text);
B.line(`call void @scr_str_release(ptr ${text})`);
B.br(done);
}
B.startBlock(labels.get(DYN_KIND.BYTES)!);
+39 -3
View File
@@ -50,7 +50,7 @@ import {
STRING,
VOID,
} from "../../ir/ir.js";
import { moduleRuntimeFeatures } from "../../ir/runtime-features.js";
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
@@ -200,7 +200,12 @@ import {
type LocalArrayRead,
} from "./local-array-reads.js";
import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js";
import { emitDenseArrayGet, emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js";
import {
emitDenseArrayGet,
emitDenseArrayPop,
emitDenseArraySet,
emitDenseSlotUnlink,
} from "./dense-array-access.js";
import {
emitStackMapRead,
findMapReadLifetimes,
@@ -885,6 +890,9 @@ export class LlEmitter {
/** setTimeout and friends appeared somewhere: main must run the event
* loop even in programs with no async functions. */
usesTimers = false;
/** The program calls mock.timers members (moduleUsesMockTimers): timer
* calls emit scr_test.c's mock-aware entry points. */
readonly mockTimers: boolean;
/** Emitted ref-kind resolve thunks for new Promise, interned per inner
* typeKey → thunk symbol. */
readonly resolveThunks = new Map<string, string>();
@@ -1111,6 +1119,7 @@ export class LlEmitter {
}),
);
this.nullableUnions = new NullableUnions(mod);
this.mockTimers = moduleUsesMockTimers(mod);
this.debug =
options.debugSources === undefined
? null
@@ -2065,7 +2074,10 @@ export class LlEmitter {
if (usesHttp) this.declare(`declare void @scr_http_dyn_install()`);
if (usesFetch) this.declare(`declare void @scr_fetch_install()`);
if (usesNodeTest) this.declare(`declare i32 @scr_test_exit_code()`);
if (usesNodeTest) {
this.declare(`declare i32 @scr_test_exit_code()`);
this.declare(`declare void @scr_test_init()`);
}
if (snapshotsTlsCa) {
this.declare(`declare void @scr_tls_ca_install()`);
}
@@ -2666,6 +2678,8 @@ export class LlEmitter {
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
...(usesStream ? [` call void @scr_stream_install()`] : []),
` call void @scr_lib_init(i32 %argc, ptr %argv)`,
// node:test takes Node's --test-update-snapshots out of argv first.
...(usesNodeTest ? [` call void @scr_test_init()`] : []),
...(asyncEntry
? [` %top = call ptr @${mangleAsyncSpawn(this.mod.entry)}()`]
: [` call void @${mangleFunction(this.mod.entry)}()`]),
@@ -8125,6 +8139,28 @@ export class LlEmitter {
B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
B.condBr(same, done, resize);
B.startBlock(resize);
if (s.pop === true) {
// A discarded pop's `length - 1` on a packed array removes the
// last element inline, then releases it.
const call = B.newLabel("arr.len.call"),
pop = B.newLabel("arr.len.pop");
const lastNumber = B.tmp(),
dropsLast = B.tmp();
B.line(`${lastNumber} = fsub double ${lenNumber}, 1.0`);
B.line(`${dropsLast} = fcmp oeq double ${length.name}, ${lastNumber}`);
B.condBr(dropsLast, pop, call);
B.startBlock(pop);
const elem = s.arr.type.elem;
const acc = elemAccess(elem);
const raw = emitDenseArrayPop(this, arr.name, acc, call, call);
if (acc === "ref" && isRefCounted(elem)) {
const old = B.tmp();
B.line(`${old} = inttoptr i64 ${raw} to ptr`);
this.releaseValue(old, elem);
}
B.br(done);
B.startBlock(call);
}
B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`);
this.emitPendingCheck();
B.br(done);
@@ -49,7 +49,11 @@ import {
STRING_UTF16_LEN_DECL,
} from "./string-reads.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { emitDenseArrayNumber, emitDenseArrayPush } from "./dense-array-access.js";
import {
emitDenseArrayNumber,
emitDenseArrayPop,
emitDenseArrayPush,
} from "./dense-array-access.js";
import { emitUnionWiden } from "./expr-records.js";
export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string {
@@ -667,19 +671,38 @@ export function emitArrIntrinsic(
const rawSlot = B.slot();
const resultSlot = B.slot();
B.entryAllocas.push(`${rawSlot} = alloca i64`, `${resultSlot} = alloca ptr`);
B.line(`store i64 0, ptr ${rawSlot}`);
const state = B.tmp();
const has = B.tmp();
B.line(`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})`);
B.line(`${has} = icmp eq i8 ${state}, 1`);
const lp = B.newLabel("remove.p");
const la = B.newLabel("remove.a");
const lj = B.newLabel("remove.j");
// pop answers an empty or packed array inline; shift and every other
// pop take the runtime call.
let denseRaw: string | null = null;
let denseLabel = "";
let slowLabel = "";
if (method === "pop") {
slowLabel = B.newLabel("remove.slow");
denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel);
denseLabel = B.currentLabel();
B.br(lp);
B.startBlock(slowLabel);
}
B.line(`store i64 0, ptr ${rawSlot}`);
const state = B.tmp();
const has = B.tmp();
const slowRaw = B.tmp();
B.line(
`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})${denseRaw === null ? "" : " cold"}`,
);
B.line(`${slowRaw} = load i64, ptr ${rawSlot}`);
B.line(`${has} = icmp eq i8 ${state}, 1`);
B.condBr(has, lp, la);
B.startBlock(lp);
const raw = B.tmp();
let raw = slowRaw;
const value = B.tmp();
B.line(`${raw} = load i64, ptr ${rawSlot}`);
if (denseRaw !== null) {
raw = B.tmp();
B.line(`${raw} = phi i64 [ ${denseRaw}, %${denseLabel} ], [ ${slowRaw}, %${slowLabel} ]`);
}
if (into) {
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
let bits = raw;
@@ -11,7 +11,12 @@ import { InternalCompilerError } from "../../errors.js";
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
import { ASSERT_VERDICT_LIB_FNS, LIB_FN_SYMS, USES_TIMERS_LIB_FNS } from "./lib-shared.js";
import {
ASSERT_VERDICT_LIB_FNS,
LIB_FN_SYMS,
mockTimerSym,
USES_TIMERS_LIB_FNS,
} from "./lib-shared.js";
import { isDynUndefined } from "../../ir/runtime-features.js";
import { DYN_KIND } from "./dyn.js";
import { rawBytes } from "./lib-abi.js";
@@ -431,7 +436,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) {
throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
}
const sym = LIB_FN_SYMS[e.fn];
const sym = mockTimerSym(host, e) ?? LIB_FN_SYMS[e.fn];
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
? emitBorrowedInputs(host, e.args)
@@ -4,6 +4,7 @@ import { undefinedArmTag } from "../../ir/analysis.js";
import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js";
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { mockTimerSym } from "./lib-shared.js";
export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
@@ -372,14 +373,16 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
host.usesTimers = true;
const cb = host.emitExpr(e.args[0]!);
host.moveTemp(cb);
const sym = {
"timers.setTimeout": "scr_set_timeout",
"timers.setInterval": "scr_set_interval",
"timers.setTimeoutHandle": "scr_set_timeout_handle",
"timers.setImmediate": "scr_set_immediate",
"process.nextTick": "scr_next_tick",
"timers.queueMicrotask": "scr_queue_microtask",
}[e.fn];
const sym =
mockTimerSym(host, e) ??
{
"timers.setTimeout": "scr_set_timeout",
"timers.setInterval": "scr_set_interval",
"timers.setTimeoutHandle": "scr_set_timeout_handle",
"timers.setImmediate": "scr_set_immediate",
"process.nextTick": "scr_next_tick",
"timers.queueMicrotask": "scr_queue_microtask",
}[e.fn];
const rest = e.args.slice(1).map((a) => host.emitExpr(a));
const argList = [`ptr ${cb.name}`, ...rest.map((a) => `${host.llType(a.type)} ${a.name}`)].join(
", ",
@@ -405,21 +408,24 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
// The chaining forms: bookkeep, then yield the handle itself (the
// C comma expressions).
const h = host.emitExpr(e.args[0]!);
const sym = {
"timers.unref": "scr_timer_unref",
"timers.ref": "scr_timer_ref",
"timers.refresh": "scr_timer_refresh",
"timers.immediateUnref": "scr_immediate_unref",
"timers.immediateRef": "scr_immediate_ref",
}[e.fn];
const sym =
mockTimerSym(host, e) ??
{
"timers.unref": "scr_timer_unref",
"timers.ref": "scr_timer_ref",
"timers.refresh": "scr_timer_refresh",
"timers.immediateUnref": "scr_immediate_unref",
"timers.immediateRef": "scr_immediate_ref",
}[e.fn];
host.declare(`declare void @${sym}(double)`);
B.line(`call void @${sym}(double ${h.name})`);
return { name: h.name, type: e.type };
}
if (e.fn === "timers.clearTimeout" || e.fn === "timers.clearInterval") {
const h = host.emitExpr(e.args[0]!);
host.declare(`declare void @scr_clear_interval(double)`);
B.line(`call void @scr_clear_interval(double ${h.name})`);
const sym = mockTimerSym(host, e) ?? "scr_clear_interval";
host.declare(`declare void @${sym}(double)`);
B.line(`call void @${sym}(double ${h.name})`);
return { name: "", type: e.type };
}
if (e.fn === "timers.clearNoop") {
@@ -200,6 +200,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"os.userHomedir": "scr_os_user_homedir",
"os.tmpdir": "scr_os_tmpdir",
"process.argv": "scr_process_argv",
"process.execArgv": "scr_process_exec_argv",
"process.platform": "scr_process_platform",
"process.cwd": "scr_process_cwd",
"process.pid": "scr_process_pid",
@@ -945,6 +946,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"worker.getEnvironmentData": "scr_worker_get_environment_data",
"worker.markUntransferable": "scr_worker_mark_untransferable",
"worker.isMarkedUntransferable": "scr_worker_is_marked_untransferable",
"dyn.markUncloneable": "scr_mark_uncloneable",
"stream.workerStdio": "scr_stream_worker_stdio",
"worker.broadcastChannel": "scr_worker_broadcast_channel",
"event.broadcastInstall": "scr_et_broadcast_install",
@@ -976,6 +978,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"emitter.getMax": "scr_emitter_get_max",
"emitter.setDefaultMax": "scr_emitter_set_default_max",
"emitter.getDefaultMax": "scr_emitter_get_default_max",
"emitter.esmDefaultMax": "scr_emitter_esm_default_max_get",
"emitter.esmSync": "scr_emitter_esm_sync",
"emitter.errorMonitor": "scr_emitter_error_monitor",
// node:stream (scr_stream.c): receivers borrowed, chunks borrowed,
// chaining forms answer the receiver +1. The throwing members
@@ -1238,10 +1242,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
// loop gate already runs) and diagnostics_channel (scr_dc.c): all
// generic shapes; the throwing members ride the pending check.
"tp.setTimeout": "scr_tp_set_timeout",
"tp.schedulerWait": "scr_tp_set_timeout",
"tp.setImmediate": "scr_tp_set_immediate",
"tp.setTimeoutValue": "scr_tp_set_timeout_value",
"tp.setImmediateValue": "scr_tp_set_immediate_value",
"tp.setTimeoutSignal": "scr_tp_set_timeout_signal",
"tp.schedulerWaitValue": "scr_tp_set_timeout_value",
"tp.schedulerWaitSignal": "scr_tp_set_timeout_signal",
"tp.setImmediateSignal": "scr_tp_set_immediate_signal",
// stream/promises' finished form (+1 promise; may-throw pending check;
// the pipeline form needs the stream-array compound and emits below).
@@ -1394,6 +1401,40 @@ export const LIB_FN_SYMS: Record<string, string> = {
"timers.immediatePromise": "scr_immediate_promise",
};
/** The mock-aware entry points (scr_test.c) that the timer calls of a
* program using mock.timers emit in place of the loop's functions; each
* falls back to the loop's function while its API is not mocked. */
export const MOCK_TIMER_SYMS: Readonly<Record<string, string>> = {
"timers.setTimeout": "scr_test_mt_set_timeout",
"timers.setTimeoutHandle": "scr_test_mt_set_timeout_handle",
"timers.setInterval": "scr_test_mt_set_interval",
"timers.clearTimeout": "scr_test_mt_clear_timeout",
"timers.clearInterval": "scr_test_mt_clear_interval",
"timers.unref": "scr_test_mt_unref",
"timers.ref": "scr_test_mt_ref",
"timers.refresh": "scr_test_mt_refresh",
"timers.hasRef": "scr_test_mt_has_ref",
"timers.setImmediate": "scr_test_mt_set_immediate",
"timers.clearImmediate": "scr_test_mt_clear_immediate",
"timers.immediateUnref": "scr_test_mt_immediate_unref",
"timers.immediateRef": "scr_test_mt_immediate_ref",
"timers.immediateHasRef": "scr_test_mt_immediate_has_ref",
"date.now": "scr_test_mt_date_now",
"date.newNow": "scr_test_mt_date_now",
"tp.schedulerWait": "scr_test_mt_scheduler_wait",
"tp.schedulerWaitValue": "scr_test_mt_scheduler_wait_value",
"tp.schedulerWaitSignal": "scr_tp_scheduler_wait_signal",
};
/** The mock-aware symbol for a timer call of a program that uses
* mock.timers, undefined for other calls and for `realTimer` calls. */
export function mockTimerSym(
host: { readonly mockTimers: boolean },
e: { fn: string; realTimer?: true },
): string | undefined {
return host.mockTimers && !e.realTimer ? MOCK_TIMER_SYMS[e.fn] : undefined;
}
export const USES_TIMERS_LIB_FNS = new Set<string>([
"process.emitWarning",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
@@ -66,6 +66,34 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
return finishRuntimeCall(host, e, "scr_test_ctx_mock_restore_all", a);
case "test.ctxMockReset":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_reset", a);
case "test.snapshotValue":
return finishRuntimeCall(host, e, "scr_test_snapshot_value", a);
case "test.ctxSnapshot":
return finishRuntimeCall(host, e, "scr_test_ctx_snapshot", a);
case "test.fileSnapshot":
return finishRuntimeCall(host, e, "scr_test_file_snapshot", a);
case "test.snapshotSerialize":
return finishRuntimeCall(host, e, "scr_test_snapshot_serialize", a);
case "test.mockTimersEnable":
return finishRuntimeCall(host, e, "scr_test_mock_timers_enable", a);
case "test.mockTimersTick":
return finishRuntimeCall(host, e, "scr_test_mock_timers_tick", a);
case "test.mockTimersRunAll":
return finishRuntimeCall(host, e, "scr_test_mock_timers_run_all", a);
case "test.mockTimersReset":
return finishRuntimeCall(host, e, "scr_test_mock_timers_reset", a);
case "test.mockTimersSetTime":
return finishRuntimeCall(host, e, "scr_test_mock_timers_set_time", a);
case "test.ctxMockTimersEnable":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_enable", a);
case "test.ctxMockTimersTick":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_tick", a);
case "test.ctxMockTimersRunAll":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_run_all", a);
case "test.ctxMockTimersReset":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_reset", a);
case "test.ctxMockTimersSetTime":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_set_time", a);
default:
throw new InternalCompilerError(`unhandled test call ${e.fn}`);
}
@@ -106,6 +106,7 @@ const INERT_LIB_CALLS = new Set([
"regex.groupParseInt",
"process.cwd",
"process.argv",
"process.execArgv",
"process.platform",
"process.execPath",
"process.isTTY",
+19 -24
View File
@@ -64,9 +64,6 @@ export class LlWalkers {
private get S(): "i32" | "i64" {
return this.host.sizeType;
}
private abiOffset(native64: number, wasm32: number): number {
return this.S === "i32" ? wasm32 : native64;
}
/** The value-parameter LLVM type of a writer for `t`. */
private valTy(t: IrType): string {
@@ -220,10 +217,10 @@ export class LlWalkers {
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`);
break;
case "dyn":
// Overflow values under an `unknown` index signature: the checked-dynamic tree
// serializes itself (runtime walker). Bare stringify of dyn stays
// frontend-fenced; this writer is reachable only through overflow
// entries.
// A checked-dynamic value serializes itself (runtime walker).
// Bare stringify of dyn stays frontend-fenced, and overflow
// entries under an `unknown` index signature go through
// scr_jb_put_dyn_entry, which also runs the value's toJSON.
this.host.declare(`declare void @scr_jb_put_dyn(ptr, ptr)`);
B.line(`call void @scr_jb_put_dyn(ptr %b, ptr %v)`);
break;
@@ -494,10 +491,13 @@ export class LlWalkers {
}
/** Whether the walker for `t` can leave an exception pending: cyclic
* values, and records serialized through a kept checked source. */
* values, records serialized through a kept checked source, and
* checked-dynamic values (toJSON methods, getters, BigInts). */
jsonWriteMayThrow(t: IrType, seen = new Set<string>()): boolean {
if (traceAdapter(this.host, t) !== null) return true;
switch (t.kind) {
case "dyn":
return true;
case "array":
return this.jsonWriteMayThrow(t.elem, seen);
case "union":
@@ -537,6 +537,15 @@ export class LlWalkers {
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
if (iv.kind === "dyn") {
// The runtime writes every entry with JSON.stringify's member rules:
// each value's toJSON runs with its key before the entry is dropped
// or written, and containers below it join the circular-structure
// stack. It may leave an exception pending.
host.declare(`declare void @scr_jb_put_dyn_entries(ptr, ptr, ptr, ptr)`);
B.line(`call void @scr_jb_put_dyn_entries(ptr %b, ptr %v, ptr ${ovf}, ptr ${first})`);
return;
}
host.declare(`declare ptr @scr_map_keys_js_order(ptr)`);
host.declare(`declare double @scr_arr_len(ptr)`);
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
@@ -576,23 +585,9 @@ export class LlWalkers {
B.line(`${val} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${k}) ; value (+1)`);
}
// Undefined-valued entries drop, exactly the optional-field rule: a
// dyn value whose dyn kind follows its size_t RC word (enum
// member 6 — scr_runtime.h's ScrDynKind), or a union holding its
// undefined arm.
// union holding its undefined arm.
let skipUndef: string | null = null;
if (iv.kind === "dyn") {
const kp = B.tmp();
const kd = B.tmp();
const isu = B.tmp();
B.line(
`${kp} = getelementptr inbounds i8, ptr ${val}, i64 ${this.abiOffset(8, 4)} ; ->kind`,
);
B.line(`${kd} = load i32, ptr ${kp}`);
B.line(
`${isu} = icmp eq i32 ${kd}, 6 ; SCR_DYN_UNDEF (NULL is 0 — null members DO serialize)`,
);
skipUndef = isu;
} else if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) {
if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) {
const tag = this.unionTag(B, val, iv.unionId);
const isu = B.tmp();
B.line(`${isu} = icmp eq i32 ${tag}, ${undefinedArmTag(iv, this.host.unionsById)}`);
@@ -2,6 +2,7 @@ import { lowerReadlineCursorCall } from "./builtins/readline.js";
import {
lowerEnvironmentData,
lowerReceiveMessageOnPort,
lowerUncloneable,
lowerUntransferable,
} from "./builtins/workers.js";
import * as ts from "../ts7/adapter.js";
@@ -117,6 +118,9 @@ export function lowerBuiltinCall(
) {
return lowerUntransferable(lowerer, expr, bi.member);
}
if (bi.module === "worker_threads" && bi.member === "markAsUncloneable") {
return lowerUncloneable(lowerer, expr);
}
const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member);
if (!builtinFn) {
lowerer.noLowering(
@@ -114,6 +114,24 @@ export function builtinImportOf(
return null;
}
/** The ES module binding kind of `ident`: "namespace" for a namespace
* import (`import * as events from "node:events"`), "named" for a named
* import (`import { defaultMaxListeners } from "node:events"`, also through
* a re-exporting module), null otherwise. Namespace members and named
* imports of a builtin read its ES module namespace, which holds copies of
* the module's exports (module.syncBuiltinESMExports refreshes them), where
* default imports and CommonJS bindings read the module object itself. */
export function esmImportBindingKind(
lowerer: Lowerer,
ident: ts.Identifier,
): "namespace" | "named" | null {
const symbol = lowerer.checker.getSymbolAtLocation(ident);
const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined;
if (decl === undefined) return null;
if (ts.isNamespaceImport(decl)) return "namespace";
return ts.isImportSpecifier(decl) ? "named" : null;
}
/** True for `const <name> = require("<builtin>").<member>` — the
* member-binding require import builtinImportOf resolves. Both
* declaration walks (lowerVarDecl, collectGlobals) skip these: like a
@@ -42,9 +42,10 @@ import { stripTypeCasts } from "./arguments.js";
/** node:module's two compiler-only calls. isBuiltin compares one evaluated
* string against the pinned Node 24 list (bare builtins also accept their
* node: spelling; prefix-only entries do not gain a bare alias).
* syncBuiltinESMExports is observably a no-op inside the static surface:
* builtin exports cannot be mutated, so its only supported behavior is
* evaluating no arguments and returning undefined. */
* syncBuiltinESMExports takes no arguments and returns undefined; as a
* statement it refreshes the events module's ES module namespace copy of
* defaultMaxListeners (lowerExprStatement), the one builtin export a program
* can change. */
export function lowerNodeModuleCall(
lowerer: Lowerer,
expr: ts.CallExpression,
@@ -64,7 +65,7 @@ export function lowerNodeModuleCall(
lowerer.noLowering(
"module.syncBuiltinESMExports used as a value",
expr,
"call syncBuiltinESMExports() as its own statement; the supported static effect is a no-op",
"call syncBuiltinESMExports() as its own statement",
);
}
return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc };
@@ -362,10 +362,10 @@ export function lowerProcessProperty(
if (member === "connected") {
return { kind: "libCall", fn: "process.connected", args: [], type: BOOL, loc };
}
// process.execArgv: the extra CLI arguments Node itself consumed — a
// compiled binary consumed none, so the honest answer is a fresh [].
// process.execArgv: the Node runtime flags kept out of process.argv —
// a leading --test-update-snapshots in node:test programs, else [].
if (member === "execArgv") {
return { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc };
return { kind: "libCall", fn: "process.execArgv", args: [], type: arrayOf(STRING), loc };
}
// process._exiting — the runtime's exit-sequence flag (true while
// 'exit' listeners run), Node's own undocumented member.
@@ -308,12 +308,15 @@ function lowerSchedulerCall(
const promiseVoid: IrType = { kind: "promise", inner: VOID };
if (member === "scheduler.yield")
return { kind: "libCall", fn: "tp.setImmediate", args: [], type: promiseVoid, loc };
// The scheduler.wait libCalls emit timers/promises setTimeout's runtime
// calls; mock.timers mocks scheduler.wait and leaves setTimeout's named
// and namespace imports real. An omitted delay is NaN, Node's undefined.
const [delayNode, optionsNode] = expr.arguments;
const ms: IrExpr = delayNode
? lowerer.lowerExprExpecting(delayNode, F64)
: { kind: "numLit", value: 1, type: F64, loc };
: { kind: "numLit", value: NaN, type: F64, loc };
if (optionsNode === undefined)
return { kind: "libCall", fn: "tp.setTimeout", args: [ms], type: promiseVoid, loc };
return { kind: "libCall", fn: "tp.schedulerWait", args: [ms], type: promiseVoid, loc };
const { ref, signal } = timersPromisesOptions(lowerer, member, optionsNode, loc);
const value: IrExpr = {
kind: "intrinsic",
@@ -324,7 +327,7 @@ function lowerSchedulerCall(
};
return {
kind: "libCall",
fn: signal === null ? "tp.setTimeoutValue" : "tp.setTimeoutSignal",
fn: signal === null ? "tp.schedulerWaitValue" : "tp.schedulerWaitSignal",
args: signal === null ? [ms, value, ref] : [ms, value, ref, signal],
type: promiseVoid,
loc,
@@ -328,6 +328,25 @@ export function lowerEnvironmentData(
};
}
/** markAsUncloneable(value): structuredClone and postMessage then throw
* DataCloneError for the value (scr_json.c). It needs no worker runtime. */
export function lowerUncloneable(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
const args = call.arguments;
if (args.length > 1 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(`markAsUncloneable with ${args.length} arguments`, call);
}
const loc = locOf(call);
const value: IrExpr = args[0]
? lowerer.lowerExprExpecting(args[0], DYN)
: {
kind: "dynFrom",
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: DYN,
loc,
};
return { kind: "libCall", fn: "dyn.markUncloneable", args: [value], type: VOID, loc };
}
/** markAsUntransferable(value) and isMarkedAsUntransferable(value): marks
* kept by this thread (an ArrayBuffer's on its storage). */
export function lowerUntransferable(
@@ -698,6 +698,8 @@ export class ClassDynamicDispatch {
private readonly bagClasses = new Set<string>();
private readonly bagInitializers = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>[]>();
private readonly typedPropertyBags = new Map<string, IrFunction>();
/** Per-class prototype selection for new property bags (bagPrototypeHelper). */
private readonly bagPrototypeHelpers = new Map<string, IrFunction>();
private readonly readPropertyBags: {
helper: IrFunction;
initialize: Extract<IrStmt, { kind: "fieldSet" }>;
@@ -1127,38 +1129,8 @@ export class ClassDynamicDispatch {
current.subclasses.some(
(child) => lowerer.classCanBeConstructed(child) && needsPrototype(child),
);
const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => {
const currentReceiver: IrExpr =
receiver.type.kind === "object" && receiver.type.className !== current.def.name
? {
kind: "downcast",
value: receiver,
type: { kind: "object", className: current.def.name },
loc,
}
: receiver;
let result = classPrototypeData(lowerer, current, loc, currentReceiver);
for (const child of current.subclasses) {
if (!lowerer.classCanBeConstructed(child)) continue;
const selected = prototypeFor(child, receiver, loc);
if (selected && result)
result = {
kind: "ternary",
cond: {
kind: "instanceOf",
value: receiver,
className: child.def.name,
type: BOOL,
loc,
},
then: selected,
else_: result,
type: DYN,
loc,
};
}
return result;
};
const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null =>
this.bagPrototype(lowerer, current, receiver, loc);
for (const existingInit of this.bagInitializers.get(className) ?? []) {
if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) {
const prototype = prototypeFor(info, existingInit.obj, existingInit.loc);
@@ -3922,10 +3894,20 @@ export class ClassDynamicDispatch {
obj: receiver,
value: transformExpr(initializers[0]!.value, {
stmt: (stmt) => stmt,
expr: (expr) =>
expr.kind === "dynCheck" && expr.value.kind === "varRef" && expr.value.localId === "p.0"
? receiver
: expr,
expr: (expr) => {
const checked = (e: IrExpr): boolean =>
e.kind === "dynCheck" && e.value.kind === "varRef" && e.value.localId === "p.0";
// The checked receiver, possibly cast to this class (the prototype
// helper's argument), is the typed parameter itself.
if (
expr.kind === "downcast" &&
checked(expr.value) &&
expr.type.kind === "object" &&
expr.type.className === type.className
)
return receiver;
return checked(expr) ? receiver : expr;
},
}),
loc,
};
@@ -3954,6 +3936,79 @@ export class ClassDynamicDispatch {
return helper;
}
/** The prototype a new property bag of `current` (or of the subclass the
* receiver is) starts from, or null when `current` has no prototype data.
* A class with constructible subclasses that have prototype data selects
* it through its `%class.bagPrototype` helper; any other class reads its
* own prototype data. */
private bagPrototype(
lowerer: Lowerer,
current: ClassInfo,
receiver: IrExpr,
loc: SrcLoc,
): IrExpr | null {
const arg: IrExpr =
receiver.type.kind === "object" && receiver.type.className !== current.def.name
? {
kind: "downcast",
value: receiver,
type: { kind: "object", className: current.def.name },
loc,
}
: receiver;
const helper = this.bagPrototypeHelper(lowerer, current, loc);
return helper
? { kind: "call", callee: helper.name, args: [arg], type: DYN, loc }
: classPrototypeData(lowerer, current, loc, arg);
}
/** `%class.bagPrototype:<class>(this)`: the prototype data of the most
* derived constructible class `this` is an instance of, tested from the
* last subclass to the first and then the class itself. One helper per
* class, calling its subclasses' helpers, keeps every property-bag branch
* a single call: a whole hierarchy inlined into each branch grows with the
* square of its depth. Null when the class has no prototype data, in which
* case its subclasses are not tested either, or while no constructible
* subclass has any. The body is rebuilt on each use, as classes become
* constructible. */
private bagPrototypeHelper(lowerer: Lowerer, current: ClassInfo, loc: SrcLoc): IrFunction | null {
const type: IrType = { kind: "object", className: current.def.name };
const self = varRef("this", type, loc);
const own = classPrototypeData(lowerer, current, loc, self);
if (!own) return null;
const body: IrStmt[] = [];
const children = current.subclasses.filter((child) => lowerer.classCanBeConstructed(child));
for (const child of children.reverse()) {
const selected = this.bagPrototype(lowerer, child, self, loc);
if (!selected) continue;
body.push({
kind: "if",
cond: { kind: "instanceOf", value: self, className: child.def.name, type: BOOL, loc },
then: [{ kind: "return", value: selected, loc }],
else_: null,
loc,
});
}
let helper = this.bagPrototypeHelpers.get(current.def.name);
if (!helper && body.length === 0) return null;
body.push({ kind: "return", value: own, loc });
if (!helper) {
helper = {
name: `%class.bagPrototype:${current.def.name}`,
params: [{ localId: "this", name: "this", type }],
returnType: DYN,
locals: [{ id: "this", name: "this", type, mutable: false }],
body,
loc,
};
this.bagPrototypeHelpers.set(current.def.name, helper);
this.generated.add(helper);
lowerer.liftedFns.push(helper);
}
helper.body = body;
return helper;
}
/** Prototype state may appear in a later reachable body, so read-only bags
* settle only after the dispatch worklist closes. A read never stores: an
* absent empty bag stays undefined (callers test it), and an absent
@@ -13,6 +13,7 @@ import {
lowerTimersMemberCall,
TIMER_MODULE_MEMBERS,
} from "./lower-timers.js";
import { lowerMockTimersElementCall } from "./lower-test.js";
import { objectEnumerationReceiver } from "./object-enumeration-receiver.js";
import { InternalCompilerError } from "../../errors.js";
import { isFsStreamMember, lowerFsStreamMethodCall } from "./builtins/fs-streams.js";
@@ -1521,6 +1522,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// The element spelling of an unsupported native Web method must fence
// before lowering the callee into a checked-dynamic keyed read.
if (ts.isElementAccessExpression(expr.expression)) {
const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression);
if (timersDispose) return timersDispose;
const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr);
if (symbolMethod) return symbolMethod;
lowerer.fenceUnsupportedFetchConstructorMember(expr.expression);
@@ -7864,7 +7864,6 @@ export function superCallStmt(
args: IrExpr[],
loc: SrcLoc,
): IrStmt {
const base = superBaseOf(info)!;
const thisRef: IrExpr = {
kind: "varRef",
localId: thisLocal.id,
@@ -7909,6 +7908,8 @@ export function superCallStmt(
},
loc,
};
// A callable base has no class info, so the base is read after that case.
const base = superBaseOf(info)!;
if (base.builtinError) {
// super(message) into the runtime-provided Error constructor: stamps
// name/message on the (already-allocated) object. Receiver + message
@@ -8,6 +8,7 @@ import { lowerEventStaticProperty } from "./builtins/numbers.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
import { lowerWorkerMetadata } from "./builtins/workers.js";
import { esmImportBindingKind } from "./builtins/module-bindings.js";
import { lowerReadlineProperty } from "./builtins/readline.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
@@ -649,6 +650,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// have non-stdlib symbols and keep the ordinary path.
let tested = expr.expression;
while (ts.isParenthesizedExpression(tested)) tested = tested.expression;
// `typeof zlib.Inflate` / `typeof zlib.createGzip`: an export of a
// Node builtin module read through a plain binding (an import or
// require result). The read has no side effects, and Node's answer for
// every exported function and class is "function".
if (
ts.isPropertyAccessExpression(tested) &&
ts.isIdentifier(tested.expression) &&
lowerer.isStdlibMember(tested)
) {
const sym = lowerer.checker.getSymbolAtLocation(tested.name);
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
if (
decls.some(isBuiltinModuleValueDecl) &&
decls.every((decl) => builtinModuleExportParent(decl) !== undefined)
) {
const t = lowerer.typeOf(tested);
if (
lowerer.checker.getCallSignatures(t).length > 0 ||
lowerer.checker.getConstructSignatures(t).length > 0
) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
}
}
if (ts.isIdentifier(tested)) {
const sym = lowerer.checker.getSymbolAtLocation(tested);
if (!sym) {
@@ -1157,6 +1182,20 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const value = lowerWorkerMetadata(bi.member, loc);
if (value) return value;
}
// A named import of defaultMaxListeners reads the ES module
// namespace's copy. A CommonJS destructured or member binding holds
// the value read when its declaration ran, which these bindings do
// not store.
if (bi.module === "events" && bi.member === "defaultMaxListeners") {
if (esmImportBindingKind(lowerer, expr) === "named") {
return { kind: "libCall", fn: "emitter.esmDefaultMax", args: [], type: F64, loc };
}
lowerer.noLowering(
"events.defaultMaxListeners through a CommonJS destructured or member binding",
expr,
"read require('events').defaultMaxListeners or EventEmitter.defaultMaxListeners where the value is needed",
);
}
const c = builtinModuleConstOf(lowerer, bi.module, bi.member);
if (c !== undefined) return builtinConstLit(c, loc);
// module.builtinModules — the baked Node v24 list, a fresh
@@ -10584,7 +10623,14 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc };
if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) {
// A Date converts to a new checked-dynamic Date on each crossing, so
// comparisons keep their Date fence instead of comparing copies.
if (
left.type.kind !== "date" &&
right.type.kind !== "date" &&
lowerer.dynConvertible(left.type) &&
lowerer.dynConvertible(right.type)
) {
return {
kind: "libCall",
fn: "dyn.compare",
@@ -10834,7 +10880,14 @@ export function lowerStrictEquality(
loc,
};
}
if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) {
// A Date converts to a new checked-dynamic Date on each crossing; its
// identity has no representation, so equality keeps the fence.
if (
idLeft.type.kind !== "date" &&
idRight.type.kind !== "date" &&
lowerer.dynConvertible(idLeft.type) &&
lowerer.dynConvertible(idRight.type)
) {
return {
kind: "dynScalarEq",
left: lowerer.coerceToExpected(idLeft, DYN),
@@ -15819,3 +15872,20 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, loc: SrcLoc): IrE
}
return { kind: "call", callee: name, args: [source], type: DYN, loc };
}
/** The `declare module "..."` body (or nested namespace body) a module
* export's declaration sits directly in, or undefined. */
function builtinModuleExportParent(decl: ts.Node): ts.ModuleBlock | undefined {
const parent = ts.isVariableDeclaration(decl) ? decl.parent?.parent?.parent : decl.parent;
return parent !== undefined && ts.isModuleBlock(parent) ? parent : undefined;
}
/** A function, class or variable exported by a builtin module's body. */
function isBuiltinModuleValueDecl(decl: ts.Node): boolean {
return (
builtinModuleExportParent(decl) !== undefined &&
(ts.isFunctionDeclaration(decl) ||
ts.isClassDeclaration(decl) ||
ts.isVariableDeclaration(decl))
);
}
@@ -32,7 +32,8 @@ import {
type IrType,
type SrcLoc,
} from "../../ir/ir.js";
import { countedFor, dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
import { countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
import type { IrLibFn } from "../../ir/builtin-signatures.js";
import { mockType as mockHybridType } from "../mock-shapes.js";
import { locOf } from "../program.js";
import { resultIsDiscarded } from "./call-position.js";
@@ -862,6 +863,157 @@ export function lowerMockTrackerCall(
};
}
const MOCK_TIMERS_HINT =
"enable, tick, runAll, reset and setTime are the supported mock.timers members";
const MOCK_TIMERS_APIS_HINT =
"list the mocked APIs as string literals: enable({ apis: ['setTimeout', 'Date'] })";
/** The codes scr_test.c reads for the APIs enable() mocks, in Node's
* SUPPORTED_APIS order. */
const MOCK_TIMERS_API_CODES: Readonly<Record<string, string>> = {
setTimeout: "T",
setInterval: "I",
setImmediate: "M",
Date: "D",
"scheduler.wait": "W",
"AbortSignal.timeout": "A",
};
/** enable() and enable({ now }) without `apis` mock every API. */
const MOCK_TIMERS_ALL_APIS = "TIMDWA";
/** `mock.timers.enable(...)` and the other MockTimers members, on the
* module's `mock` (tracker null) and t.mock. enable() takes no options or
* an object literal whose `apis`, when given, is an array literal of API
* names. */
export function lowerMockTimersCall(
lowerer: Lowerer,
call: ts.CallExpression,
member: string,
what: string,
tracker: () => MockTracker,
sym: ts.Symbol | null,
): IrExpr {
const loc = locOf(call);
const args = call.arguments;
const optionalNumber = (fallback: number): IrExpr[] => {
if (args.length > 1)
lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call);
return [args.length === 1 ? lowerer.lowerExprExpecting(args[0]!, F64) : numLit(fallback, loc)];
};
if (!["enable", "tick", "setTime", "runAll", "reset"].includes(member)) {
lowerer.noLowering(`${what}.${member}`, call, MOCK_TIMERS_HINT, sym);
}
// The receiver evaluates before the arguments.
const t = tracker();
let rest: IrExpr[];
switch (member) {
case "enable":
rest = lowerMockTimersOptions(lowerer, call, what);
break;
case "tick":
rest = optionalNumber(1);
break;
case "setTime":
rest = optionalNumber(0);
break;
default:
if (args.length > 0) {
lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call);
}
rest = [];
}
const name = member[0]!.toUpperCase() + member.slice(1);
return {
kind: "libCall",
fn: (t ? `test.ctxMockTimers${name}` : `test.mockTimers${name}`) as IrLibFn,
args: t ? [t, ...rest] : rest,
type: VOID,
loc,
};
}
/** enable()'s { apis, now } as the libCall's API codes, `now` and flags
* (1 = `now` given, 2 = it is a Date). */
function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] {
const loc = locOf(call);
const options = call.arguments[0];
if (call.arguments.length > 1) {
lowerer.noLowering(`${what}.enable with ${call.arguments.length} arguments`, call);
}
if (options === undefined) {
return [strLit(MOCK_TIMERS_ALL_APIS, loc), numLit(0, loc), numLit(0, loc)];
}
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
`${what}.enable with a non-literal options object`,
options,
MOCK_TIMERS_APIS_HINT,
);
}
let codes = "";
let listed = false;
let now: IrExpr = numLit(0, loc);
let flags = 0;
for (const prop of options.properties) {
// `{ apis: [...] }`, `{ "now": n }` and the `{ now }` shorthand.
const init = ts.isPropertyAssignment(prop)
? prop.initializer
: ts.isShorthandPropertyAssignment(prop) && ts.isIdentifier(prop.name)
? prop.name
: null;
const key =
init && prop.name && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
? prop.name.text
: null;
if (key === "apis") {
const list = init!;
if (!ts.isArrayLiteralExpression(list)) {
lowerer.noLowering(
`${what}.enable with a non-literal apis list`,
list,
MOCK_TIMERS_APIS_HINT,
);
}
listed = true;
for (const el of list.elements) {
if (!ts.isStringLiteralLike(el)) {
lowerer.noLowering(
`${what}.enable with a non-literal API name`,
el,
MOCK_TIMERS_APIS_HINT,
);
}
const code = MOCK_TIMERS_API_CODES[el.text] ?? "";
if (code === "") {
lowerer.noLowering(`${what}.enable with the ${el.text} API`, el, MOCK_TIMERS_APIS_HINT);
}
codes += code;
}
} else if (key === "now") {
const value = lowerer.lowerExpr(init!);
if (value.type.kind === "date") {
now = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc };
flags = 3;
} else if (value.type.kind === "f64") {
now = value;
flags = 1;
} else {
lowerer.noLowering(
`${what}.enable with a now value of this type`,
prop,
"now takes a number or a Date",
);
}
} else {
lowerer.noLowering(
`${what}.enable with this option`,
prop,
"apis and now are the supported enable options",
);
}
}
return [strLit(listed ? codes : MOCK_TIMERS_ALL_APIS, loc), now, numLit(flags, loc)];
}
/** `fn.mock.calls`: a copy of the log, as Node's getter returns. Null for
* other reads. */
export function lowerMockContextProperty(
@@ -5302,7 +5302,7 @@ function lowerDynSwitch(
loc: locOf(expression),
};
}
if (!lowerer.dynConvertible(test.type)) {
if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) {
lowerer.unsupported(
"SC1100",
expression,
@@ -6055,16 +6055,22 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
);
}
// syncBuiltinESMExports() is the other compiler-only statement: the
// call validates its zero-argument shape, then emits no code because
// the static builtin surface is immutable.
// call validates its zero-argument shape, then refreshes the events
// module's ES module namespace copy of defaultMaxListeners, the one
// builtin export a program can change.
const bi = ts.isIdentifier(expr.expression)
? lowerer.builtinImportOf(expr.expression)
: ts.isPropertyAccessExpression(expr.expression)
? lowerer.builtinMemberOf(expr.expression)
: null;
if (bi?.module === "module" && bi.member === "syncBuiltinESMExports") {
lowerNodeModuleCall(lowerer, expr, bi, locOf(expr));
return { kind: "block", body: [], loc: locOf(expr) };
const loc = locOf(expr);
lowerNodeModuleCall(lowerer, expr, bi, loc);
return {
kind: "exprStmt",
expr: { kind: "libCall", fn: "emitter.esmSync", args: [], type: VOID, loc },
loc,
};
}
}
if (
@@ -21,23 +21,26 @@ import {
type IrStmt,
type IrType,
type SrcLoc,
BOOL,
DYN,
F64,
STRING,
VOID,
isUnitType,
} from "../../ir/ir.js";
import { numLit, strLit } from "../../ir/build.js";
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
import {
lowerMockContextCall,
lowerMockContextProperty,
lowerMockFn,
lowerMockMethod,
lowerMockTimersCall,
lowerMockTrackerCall,
type MockTracker,
} from "./lower-mock.js";
const MOCK_HINT =
"mock.fn, mock.method, mock.restoreAll and mock.reset are the supported mock members";
"mock.fn, mock.method, mock.restoreAll, mock.reset and mock.timers are the supported mock members";
/** The mock tracker members that lower, for the module's `mock` (tracker
* null) and t.mock. Null for other members. */
@@ -517,7 +520,11 @@ export function lowerNodeTestModuleCall(
);
break;
case "snapshot":
lowerer.noLowering("test.snapshot", expr, "snapshot testing has no lowering");
lowerer.noLowering(
"test.snapshot",
expr,
"t.assert.snapshot and t.assert.fileSnapshot are the supported snapshot surface",
);
break;
default:
lowerer.noLowering(
@@ -556,6 +563,195 @@ export function lowerTestDirectCall(
return lowerRegistration(lowerer, expr, "test(...)", loc, 0, null);
}
/** t.assert.snapshot(value[, options]) and t.assert.fileSnapshot(value,
* path[, options]), Node's SnapshotManager assertions: the value runs
* through options.serializers, by default Node's JSON.stringify(value,
* null, 2), and String() of the result is compared with
* assert.strictEqual against the snapshot that scr_test.c reads from
* "<entry file>.snapshot" or `path`, or records in update mode. A throw
* while serializing becomes Node's ERR_INVALID_STATE. */
function lowerSnapshotAssert(
lowerer: Lowerer,
call: ts.CallExpression,
member: "snapshot" | "fileSnapshot",
testCtx: ts.Expression,
): IrExpr {
const loc = locOf(call);
const what = `t.assert.${member}`;
const t = lowerer.lowerExpr(testCtx);
const stmts: IrStmt[] = [];
const slot = (name: string, type: IrType, init: IrExpr): IrExpr => {
const local = lowerer.declareHiddenLocal(name, type);
stmts.push({ kind: "varDecl", localId: local.id, init, loc });
return varRef(local.id, type, loc);
};
// Arguments evaluate in order before the assertion serializes, as in Node.
const valueNode = call.arguments[0];
const input = slot(
"%snapshotInput",
DYN,
valueNode ? lowerer.lowerExprExpecting(valueNode, DYN) : dynUndefinedExpr(loc),
);
let path: IrExpr | null = null;
if (member === "fileSnapshot") {
const pathNode = call.arguments[1];
path = slot(
"%snapshotPath",
STRING,
pathNode ? lowerer.lowerExprExpecting(pathNode, STRING) : strLit("", loc),
);
}
const serializers =
snapshotSerializers(lowerer, call.arguments[member === "snapshot" ? 1 : 2], what)?.map((fn) =>
slot("%snapshotSerializer", DYN, fn),
) ?? null;
if (call.arguments.length > (member === "snapshot" ? 2 : 3)) {
lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call);
}
const text: IrExpr = {
kind: "toString",
operand: {
kind: "libCall",
fn: "test.snapshotSerialize",
args: [
input,
serializers === null
? dynUndefinedExpr(loc)
: { kind: "dynArrLit", elems: serializers, type: DYN, loc },
],
type: DYN,
loc,
},
type: STRING,
loc,
};
const actual = slot(
"%snapshotActual",
STRING,
member === "snapshot"
? { kind: "libCall", fn: "test.snapshotValue", args: [text], type: STRING, loc }
: text,
);
const expected: IrExpr =
member === "snapshot"
? {
kind: "libCall",
fn: "test.ctxSnapshot",
args: [t, actual, strLit(`${lowerer.entry.fileName}.snapshot`, loc)],
type: STRING,
loc,
}
: { kind: "libCall", fn: "test.fileSnapshot", args: [actual, path!], type: STRING, loc };
const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
return {
kind: "seqExpr",
stmts,
result: {
kind: "libCall",
fn: "assert.eqStr",
args: [actual, expected, no, no, strLit("", loc), no],
type: VOID,
loc,
},
type: VOID,
loc,
};
}
/** The serializer functions of a snapshot options object literal, as
* checked-dynamic values in order; null without a serializers option.
* Options that are not an object literal, a serializers list that is not
* an array literal, and other options fence. */
function snapshotSerializers(
lowerer: Lowerer,
options: ts.Expression | undefined,
what: string,
): IrExpr[] | null {
if (options === undefined) return null;
const hint = "pass the options as an object literal: { serializers: [(value) => ...] }";
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(`${what} options that are not an object literal`, options, hint);
}
let fns: IrExpr[] | null = null;
for (const prop of options.properties) {
const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null;
if (
name !== "serializers" ||
!(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop))
) {
lowerer.noLowering(`${what} options other than serializers`, prop, hint);
}
const list = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name;
if (!ts.isArrayLiteralExpression(list)) {
lowerer.noLowering(`${what} with a non-literal serializers list`, list, hint);
}
fns = list.elements.map((el) => {
if (ts.isSpreadElement(el)) lowerer.noLowering(`${what} with a spread serializer`, el, hint);
return lowerer.lowerExprExpecting(el, DYN);
});
}
return fns;
}
/** `mock.timers` (tracker null) or `t.mock.timers` on a TestContext;
* null for other expressions. */
function mockTimersReceiver(
lowerer: Lowerer,
expr: ts.Expression,
): { what: string; tracker: () => MockTracker } | null {
if (
!ts.isPropertyAccessExpression(expr) ||
expr.questionDotToken ||
expr.name.text !== "timers" ||
!lowerer.isStdlibMember(expr)
) {
return null;
}
const owner = expr.expression;
if (ts.isIdentifier(owner)) {
const bi = lowerer.builtinImportOf(owner);
return bi && bi.module === "test" && bi.member === "mock"
? { what: "mock.timers", tracker: () => null }
: null;
}
if (
ts.isPropertyAccessExpression(owner) &&
!owner.questionDotToken &&
owner.name.text === "mock" &&
lowerer.mapTypeOf(lowerer.typeOf(owner.expression))?.kind === "testCtx" &&
lowerer.isStdlibMember(owner)
) {
const testCtx = owner.expression;
return { what: "t.mock.timers", tracker: () => lowerer.lowerExpr(testCtx) };
}
return null;
}
/** `mock.timers[Symbol.dispose]()` and its t.mock twin: an alias of
* reset(), as in Node. Null for other element-access calls. */
export function lowerMockTimersElementCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.ElementAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
const timers = mockTimersReceiver(lowerer, access.expression);
if (!timers) return null;
const key = access.argumentExpression;
const isDispose =
ts.isPropertyAccessExpression(key) &&
key.name.text === "dispose" &&
lowerer.stdlibGlobalMember(key, "Symbol") === "dispose";
if (!isDispose) {
lowerer.noLowering(
`${timers.what} computed member calls`,
call,
"call timers[Symbol.dispose]()",
);
}
return lowerMockTimersCall(lowerer, call, "reset", timers.what, timers.tracker, null);
}
/** Method-position node:test calls — one entry in lower-calls.ts's
* intrinsic chain: skip/todo/only twins on NAMED import bindings
* (`test.skip(...)` where `test` came from `{ test }`), the TestContext
@@ -634,6 +830,13 @@ export function lowerTestMethodCall(
);
if (served) return served;
}
// mock.timers.enable(...) and the other MockTimers members, on the
// module's `mock` and on t.mock.
const timers = mockTimersReceiver(lowerer, access.expression);
if (timers) {
const sym = lowerer.checker.getSymbolAtLocation(access.name) ?? null;
return lowerMockTimersCall(lowerer, call, member, timers.what, timers.tracker, sym);
}
// t.assert.strictEqual(...) — the callee's receiver is `t.assert` on a
// TestContext: Node's t.assert methods ARE the assert functions (bound
// to the test for its counters — not modeled; the throw is identical),
@@ -645,7 +848,10 @@ export function lowerTestMethodCall(
lowerer.mapTypeOf(lowerer.typeOf(access.expression.expression))?.kind === "testCtx" &&
lowerer.isStdlibMember(access.expression)
) {
const served = lowerer.lowerAssertModuleCall(call, { module: "assert", member }, loc);
const served =
member === "snapshot" || member === "fileSnapshot"
? lowerSnapshotAssert(lowerer, call, member, access.expression.expression)
: lowerer.lowerAssertModuleCall(call, { module: "assert", member }, loc);
// Each t.assert call counts against the test's plan (t.plan).
if (served)
return {
@@ -299,6 +299,40 @@ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLo
return null;
}
const MOCKABLE_TIMER_FNS: ReadonlySet<string> = new Set([
"timers.setTimeout",
"timers.setTimeoutHandle",
"timers.setInterval",
"timers.clearTimeout",
"timers.clearInterval",
"timers.setImmediate",
"timers.clearImmediate",
]);
/** Whether a timer call's callee is an ESM named or namespace import of
* node:timers (destructured bindings included): Node's mock.timers leaves
* those bindings alone and replaces the globals. */
function isRealTimerCallee(lowerer: Lowerer, callee: ts.Expression): boolean {
if (ts.isIdentifier(callee)) return lowerer.builtinImportOf(callee)?.module === "timers";
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) {
const sym = lowerer.checker.getSymbolAtLocation(callee.expression);
const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined;
return decl !== undefined && ts.isNamespaceImport(decl);
}
return false;
}
/** Marks a lowered timer call `realTimer` when its callee is a binding
* mock.timers does not replace. */
function markRealTimerCall(
lowerer: Lowerer,
expr: ts.CallExpression,
lowered: IrExpr | null,
): IrExpr | null {
if (lowered?.kind !== "libCall" || !MOCKABLE_TIMER_FNS.has(lowered.fn)) return lowered;
return isRealTimerCallee(lowerer, expr.expression) ? { ...lowered, realTimer: true } : lowered;
}
/** One timer call by MEMBER NAME — the shared lowering behind the ambient
* globals, the node:timers named/destructured imports, and the namespace
* form (`timers.setTimeout(...)`). Null when the member isn't a lowered
@@ -308,6 +342,15 @@ export function lowerTimersMemberCall(
expr: ts.CallExpression,
member: string,
loc: SrcLoc,
): IrExpr | null {
return markRealTimerCall(lowerer, expr, lowerTimerCall(lowerer, expr, member, loc));
}
function lowerTimerCall(
lowerer: Lowerer,
expr: ts.CallExpression,
member: string,
loc: SrcLoc,
): IrExpr | null {
// Delayed timers share callback adaptation and the 1ms default. Trailing
// arguments are captured now and delivered when the callback fires.
@@ -328,10 +371,12 @@ export function lowerTimersMemberCall(
expr.arguments[0]!,
loc,
);
// An omitted delay is NaN, Node's undefined: the loop clamps it to
// 1ms, and mock.timers queues it to run at the next tick.
const ms: IrExpr =
expr.arguments.length >= 2
? lowerer.lowerExpr(expr.arguments[1]!)
: { kind: "numLit", value: 1, type: F64, loc };
: { kind: "numLit", value: NaN, type: F64, loc };
if (member === "setInterval") {
return { kind: "libCall", fn: "timers.setInterval", args: [cb, ms], type: F64, loc };
}
@@ -389,6 +389,7 @@ import {
createRequireProgramModuleOf,
createRequireSpecOf,
fencedBuiltinImportOf,
esmImportBindingKind,
} from "./builtins/module-bindings.js";
import { stripTypeCasts } from "./builtins/arguments.js";
import { lowerBuiltinModuleCall } from "./builtins/module-calls.js";
@@ -11481,9 +11482,19 @@ export class Lowerer {
return pathModuleValue(this, `path/${bi.member}`, loc);
}
// events.defaultMaxListeners READS the process-wide default (its
// write twin routes through emitter.setDefaultMaxChk).
// write twin routes through emitter.setDefaultMaxChk). Through an ES
// module namespace it reads the namespace's copy instead.
if (bi.module === "events" && bi.member === "defaultMaxListeners") {
return { kind: "libCall", fn: "emitter.getDefaultMax", args: [], type: F64, loc };
const esm =
ts.isIdentifier(expr.expression) &&
esmImportBindingKind(this, expr.expression) === "namespace";
return {
kind: "libCall",
fn: esm ? "emitter.esmDefaultMax" : "emitter.getDefaultMax",
args: [],
type: F64,
loc,
};
}
if (bi.module === "worker_threads") {
const value = lowerWorkerMetadata(bi.member, loc);
@@ -2456,6 +2456,12 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
name: "process.argv0",
fns: ["process.argv0"],
},
{
id: "node-builtin.process.execArgv",
kind: "node-builtin",
name: "process.execArgv",
fns: ["process.execArgv"],
},
{
id: "node-builtin.process.execPath",
kind: "node-builtin",
@@ -2884,7 +2890,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
isBuiltin:
"the lowered call checks a string against the baked Node v24 list, including bare/node: aliases and prefix-only builtins",
syncBuiltinESMExports:
"the direct zero-argument call is a no-op because compiled builtin exports are immutable and cannot become stale",
"the direct zero-argument call statement refreshes the ES module namespace copy of events.defaultMaxListeners, the one builtin export a program can change",
},
child_process: {
execFile:
@@ -16,6 +16,22 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"test.sub",
"test.subEmpty",
"test.suite",
// mock.timers: Node's ERR_INVALID_STATE and ERR_INVALID_ARG_VALUE, and
// the throws of the timer callbacks tick and runAll run.
"test.ctxSnapshot",
"test.fileSnapshot",
"test.snapshotSerialize",
"test.mockTimersEnable",
"test.mockTimersTick",
"test.mockTimersRunAll",
"test.mockTimersSetTime",
"test.ctxMockTimersEnable",
"test.ctxMockTimersTick",
"test.ctxMockTimersRunAll",
"test.ctxMockTimersSetTime",
"tp.schedulerWait",
"tp.schedulerWaitValue",
"tp.schedulerWaitSignal",
"console.write",
"console.label",
"console.countReset",
@@ -1132,6 +1132,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* (scr_worker_events.c; the value borrowed). */
"worker.markUntransferable": { argTypes: [DYN], result: VOID },
"worker.isMarkedUntransferable": { argTypes: [DYN], result: BOOL },
/** markAsUncloneable(value) (scr_json.c; the value borrowed). */
"dyn.markUncloneable": { argTypes: [DYN], result: VOID },
/** before new Worker() with the stdout or stderr option: links the stream
* unit that worker.stdout and worker.stderr read through (scr_stream.c). */
"stream.workerStdio": { argTypes: [], result: VOID },
@@ -1469,6 +1471,32 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
},
"test.ctxMockRestoreAll": { argTypes: [TESTCTX_T], result: VOID },
"test.ctxMockReset": { argTypes: [TESTCTX_T], result: VOID },
// mock.timers on the module's `mock` and on t.mock. enable takes one
// code per listed API ('T' setTimeout, 'I' setInterval), the `now`
// value and its flags (1 given, 2 from a Date). enable, tick, runAll and
// setTime throw Node's errors; tick and runAll run timer callbacks.
// t.assert.snapshot and fileSnapshot: the escaped, wrapped serialized
// value, and the stored value it is compared with (snapshot: the test,
// the value, the snapshot file; fileSnapshot: the value, the path).
// Both read or, in update mode, record; missing and unreadable
// snapshots throw Node's ERR_INVALID_STATE.
"test.snapshotValue": { argTypes: [STRING], result: STRING },
"test.ctxSnapshot": { argTypes: [TESTCTX_T, STRING, STRING], result: STRING },
"test.fileSnapshot": { argTypes: [STRING, STRING], result: STRING },
// Node's serializeValue over a dyn array of serializers (undefined for
// the default JSON.stringify(value, null, 2)); a throw becomes Node's
// "The provided serializers did not generate a string." ERR_INVALID_STATE.
"test.snapshotSerialize": { argTypes: [DYN, DYN], result: DYN },
"test.mockTimersEnable": { argTypes: [STRING, F64, F64], result: VOID },
"test.mockTimersTick": { argTypes: [F64], result: VOID },
"test.mockTimersRunAll": { argTypes: [], result: VOID },
"test.mockTimersReset": { argTypes: [], result: VOID },
"test.mockTimersSetTime": { argTypes: [F64], result: VOID },
"test.ctxMockTimersEnable": { argTypes: [TESTCTX_T, STRING, F64, F64], result: VOID },
"test.ctxMockTimersTick": { argTypes: [TESTCTX_T, F64], result: VOID },
"test.ctxMockTimersRunAll": { argTypes: [TESTCTX_T], result: VOID },
"test.ctxMockTimersReset": { argTypes: [TESTCTX_T], result: VOID },
"test.ctxMockTimersSetTime": { argTypes: [TESTCTX_T, F64], result: VOID },
// node:http (scr_http.c over scr_net.c). The handler and the data
// listeners are program-dependent closures (null slots, checked in the
// libCall case); reqHeader's result is the interned string|undefined
@@ -2869,6 +2897,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
},
"fileHandle.stat": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: STATS_T } },
"process.argv": { argTypes: [], result: arrayOf(STRING) },
// The Node runtime flag a node:test binary took out of process.argv
// (--test-update-snapshots), or [] — a fresh array per read.
"process.execArgv": { argTypes: [], result: arrayOf(STRING) },
"process.platform": { argTypes: [], result: STRING },
// The one libCall whose result type is program-dependent (union ids are
// per-module): the `result` here is a placeholder — the libCall case
@@ -3380,6 +3411,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"emitter.getMax": { argTypes: [null], result: F64 },
"emitter.setDefaultMax": { argTypes: [F64], result: VOID },
"emitter.getDefaultMax": { argTypes: [], result: F64 },
// The ES module namespace's copy of defaultMaxListeners, and
// module.syncBuiltinESMExports() refreshing it.
"emitter.esmDefaultMax": { argTypes: [], result: F64 },
"emitter.esmSync": { argTypes: [], result: VOID },
/** The events.errorMonitor event key: an immortal string the runtime
* registry matches by identity, so no string event name reaches it. */
"emitter.errorMonitor": { argTypes: [], result: STRING },
@@ -3933,6 +3968,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* through the immediate queue (fires before due timers of later loop
* turns, Node's check phase). Neither throws. */
"tp.setTimeout": { argTypes: [F64], result: { kind: "promise", inner: VOID } },
// scheduler.wait(delay): tp.setTimeout's promise. It throws Node's
// ERR_INVALID_THIS once mock.timers has restored the real scheduler.wait.
"tp.schedulerWait": { argTypes: [F64], result: { kind: "promise", inner: VOID } },
"tp.setImmediate": { argTypes: [], result: { kind: "promise", inner: VOID } },
/** events.once (scr_events_emitter.c): [emitter, name, boxer,
* errorBoxer, listenForError] → a pending promise of any[]; the boxers
@@ -3949,6 +3987,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* AbortError (scr_event_target.c). */
"tp.setTimeoutSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID },
"tp.setImmediateSignal": { argTypes: [null, BOOL, DYN], result: VOID },
/** scheduler.wait(delay, options): tp.setTimeoutValue's and
* tp.setTimeoutSignal's shapes, with tp.schedulerWait's throws. */
"tp.schedulerWaitValue": { argTypes: [F64, null, BOOL], result: VOID },
"tp.schedulerWaitSignal": { argTypes: [F64, null, BOOL, DYN], result: VOID },
/** node:diagnostics_channel (scr_dc.c, linked when any dc.* appears —
* the zlib gating precedent): a process-global name→channel registry;
* channel values are f64 handles (type-mapper.ts maps Channel to F64, the
+6
View File
@@ -3603,6 +3603,11 @@ export type IrExpr =
type: IrType;
loc: SrcLoc;
prototypeIdentityOnly?: true;
/** A timer call through an ESM named or namespace import of
* node:timers. Node's mock.timers replaces the globals and the
* module object's properties, which these bindings do not follow,
* so the call keeps the loop's timer. */
realTimer?: true;
/** An assertion call made through a node:assert Assert instance: the
* instance options the runtime applies to this call. Backends set
* them right before the runtime call, after its arguments are
@@ -4066,6 +4071,7 @@ function canBoxDynCompositeAt(
case "regex":
case "url":
case "searchParams":
case "date":
return true;
case "func":
return canBoxFuncIntoDyn(t, getRecord, getUnion, visiting);
@@ -34,6 +34,9 @@ export interface RuntimeFeatures {
parseArgs: boolean;
fsWatch: boolean;
nodeTest: boolean;
/** The program calls mock.timers members: its timer calls go through
* scr_test.c's mock-aware entry points. */
mockTimers: boolean;
dgram: boolean;
http: boolean;
http2: boolean;
@@ -205,6 +208,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
parseArgs: false,
fsWatch: false,
nodeTest: false,
mockTimers: false,
dgram: false,
http: false,
http2: false,
@@ -252,6 +256,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
fn === "rl.questionAsyncSignal" ||
fn === "http.clientSignal" ||
fn === "tp.setTimeoutSignal" ||
fn === "tp.schedulerWaitSignal" ||
fn === "tp.setImmediateSignal" ||
fn === "stream.addAbortSignal" ||
fn === "stream.addAbortSignalFn" ||
@@ -411,6 +416,9 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
if (fn.startsWith("test.")) {
features.nodeTest = true;
features.inspect = true;
if (fn.startsWith("test.mockTimers") || fn.startsWith("test.ctxMockTimers")) {
features.mockTimers = true;
}
}
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
if (fn.startsWith("tlsca.")) features.tlsCa = true;
@@ -795,6 +803,14 @@ export function moduleUsesNodeTest(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "nodeTest").nodeTest;
}
/** True when the module calls a mock.timers member (a test.mockTimers* or
* test.ctxMockTimers* libCall). The emitter then routes the timer calls
* through scr_test.c's mock-aware entry points; other programs keep the
* loop's timer functions. */
export function moduleUsesMockTimers(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "mockTimers").mockTimers;
}
/** True when the module contains any dgram.* or dns.* libCall — the
* link switch that pulls scr_dgram.c into the binary and has the emitted
* main call scr_dgram_install (native-toolchain.ts + emitter; the scr_net gating
+7 -2
View File
@@ -4156,9 +4156,12 @@ function validateFunction(
e.fn === "tp.setTimeoutValue" ||
e.fn === "tp.setImmediateValue" ||
e.fn === "tp.setTimeoutSignal" ||
e.fn === "tp.setImmediateSignal"
e.fn === "tp.setImmediateSignal" ||
e.fn === "tp.schedulerWaitValue" ||
e.fn === "tp.schedulerWaitSignal"
) {
const value = e.args[e.fn === "tp.setTimeoutValue" || e.fn === "tp.setTimeoutSignal" ? 1 : 0];
const value =
e.args[e.fn === "tp.setImmediateValue" || e.fn === "tp.setImmediateSignal" ? 0 : 1];
if (value?.type.kind !== "promise" || !typeEquals(e.type, value.type)) {
err(`libCall ${e.fn} must be the value's promise type`, e.loc);
}
@@ -5732,6 +5735,8 @@ function validateFunction(
e.fn !== "emitter.setDefaultMax" &&
e.fn !== "emitter.setDefaultMaxChk" &&
e.fn !== "emitter.getDefaultMax" &&
e.fn !== "emitter.esmDefaultMax" &&
e.fn !== "emitter.esmSync" &&
e.fn !== "emitter.errorMonitor" &&
e.fn !== "emitter.checkListener"
) {
@@ -196,6 +196,7 @@ export const LIB_NONDETERMINISTIC_PREFIXES: readonly [string, string][] = [
["blob.fileNew", "the live clock (File lastModified)"],
["process.env", "environment reads"],
["process.argv", "process.argv"],
["process.execArgv", "process.execArgv"],
["process.cwd", "process.cwd"],
["process.chdir", "process.chdir"],
["process.pid", "process identity"],
+7 -1
View File
@@ -1367,7 +1367,7 @@
"name": "module.syncBuiltinESMExports",
"status": "unsupported",
"code": "SC2020",
"note": "the direct zero-argument call is a no-op because compiled builtin exports are immutable and cannot become stale"
"note": "the direct zero-argument call statement refreshes the ES module namespace copy of events.defaultMaxListeners, the one builtin export a program can change"
},
{
"id": "node-builtin.net",
@@ -1944,6 +1944,12 @@
"status": "static",
"note": "reads, writes, deletes, and enumeration of the process environment (the process global)"
},
{
"id": "node-builtin.process.execArgv",
"kind": "node-builtin",
"name": "process.execArgv",
"status": "static"
},
{
"id": "node-builtin.process.execPath",
"kind": "node-builtin",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5681-array-pop-and-length.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5813-mock-timers-module.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5814-mock-timers-date.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5841-json-typed-dyn-members.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5844-date-unknown-values.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5846-events-esm-default-max.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5847-events-cjs-default-max.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5915-dyn-error-string-conversions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5974-typeof-module-exports.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5990-worker-uncloneable.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5991-worker-async-dispose.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6020-class-extends-function-constructor.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/native-worker-discarded-heaps.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/mock-timers-fences.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/snapshot-fences.ts"
],
"diags": []
}
+473 -14
View File
@@ -630,6 +630,14 @@ static bool scr_sa_reserve_region(void) {
* scr_sa_trim). */
static size_t scr_sa_claim_carved[SCR_SA_ARENAS];
static size_t scr_sa_carved(void);
/* A worker thread's memory, discarded when it parks (see "Discarding a
* worker's memory" below). */
typedef struct ScrSaKeep ScrSaKeep;
static _Thread_local bool scr_sa_worker; /* set by scr_sa_thread_starting */
static _Thread_local ScrSaKeep *scr_sa_keep; /* while the worker's memory is kept apart */
static void scr_sa_keep_begin(unsigned i);
static void scr_sa_keep_drained(ScrSaKeep *k, ScrSaBlock *b, unsigned c);
static bool scr_sa_drain(void);
#endif
/* Moves every block other threads freed into this arena's free lists. */
@@ -638,6 +646,13 @@ static void scr_sa_drain_remote(void) {
while (b) {
ScrSaBlock *next = b->next;
unsigned c = (unsigned)(((uintptr_t)b - scr_sa.base) >> scr_sa.shift);
#ifdef SCR_SA_TRIM
if (scr_sa_keep) {
scr_sa_keep_drained(scr_sa_keep, b, c);
b = next;
continue;
}
#endif
b->next = scr_sa.free[c];
scr_sa.free[c] = b;
b = next;
@@ -655,6 +670,15 @@ static bool scr_sa_reserve(void) {
uint32_t claimed = atomic_load_explicit(&scr_sa_claimed, memory_order_relaxed);
for (;;) {
if (claimed == UINT32_MAX) {
#ifdef SCR_SA_TRIM
/* Arenas held by discards that wait (see scr_sa_thread_park): this
* thread discards them and claims one. */
if (scr_sa_drain()) {
scr_sa_refused = false;
claimed = atomic_load_explicit(&scr_sa_claimed, memory_order_relaxed);
continue;
}
#endif
scr_sa_refused = true;
return false;
}
@@ -672,6 +696,7 @@ static bool scr_sa_reserve(void) {
scr_sa_drain_remote();
#ifdef SCR_SA_TRIM
scr_sa_claim_carved[i] = scr_sa_carved();
if (scr_sa_worker) scr_sa_keep_begin(i);
#endif
return true;
}
@@ -884,6 +909,312 @@ static void scr_sa_trim(void) {
scr_sa_arena, (int)trim, scr_sa_trim_stats[0] >> 20, scr_sa_trim_stats[1] >> 20,
scr_sa_trim_stats[2] >> 20);
}
/* Discarding a worker's memory (Linux). Script objects stay on the thread
* that allocated them: other threads reach one only through a message or an
* intern table that shares a published (immortal) graph by pointer, or a
* process-wide immortal cache, and each of those calls
* scr_sa_thread_shares. Runtime state that outlives a thread (messages,
* mailboxes, shared bytes, intern table entries) comes from the system
* allocator directly. So when a worker thread parks without having shared,
* nothing it allocated through scr_mem_* can be reachable from another
* thread, and what it never freed (ProgramLifetime instances, published
* module constants) is garbage. Such a thread's memory goes back whole:
* - when a worker claims an arena, the arena's free lists and returned
* regions are set aside, and the worker carves only past each slice's
* frontier, so its blocks of class c are [start[c], bump[c]), plus the
* blocks it takes from the free lists set aside once a slice has no bump
* room left, which it records;
* - the system blocks it allocates (large blocks, an exhausted slice) are
* recorded until it frees them;
* - a remote free into its blocks means another thread held one: the same
* as sharing;
* - when it parks without having shared, its arena stays claimed until no
* other worker thread is running (scr_sa_thread_park): returning pages
* interrupts every running thread of the process to flush its TLB, and
* glibc's trim locks the arenas other threads allocate from, so discards
* wait for the last worker of a group, which hands them to one helper;
* - the helper then returns the pages
* it carved (MADV_DONTNEED; the start of the first page, which it may
* share with older blocks, is cleared), frees the recorded system blocks,
* and parks the arena as the worker found it, with the blocks it took
* free again. A worker that shared parks
* its arena with the blocks set aside added to its free lists, and the
* ordinary trim applies.
* SCR_SA_DISCARD=0 keeps every worker's memory. SCR_SA_DISCARD=poison makes
* the carved pages inaccessible instead, and keeps the system blocks, so
* that a reference to discarded memory faults (a check for tests). */
#define SCR_SA_SYS_GONE ((void *)1) /* a removed entry of ScrSaKeep.sys */
struct ScrSaKeep {
/* The arena as the worker claimed it: free lists (to which remote frees
* of older blocks are added), carve cursors and returned regions. */
ScrSaBlock *free[SCR_SA_NCLASS];
char *bump[SCR_SA_NCLASS], *lim[SCR_SA_NCLASS];
ScrSaHoles holes[SCR_SA_NCLASS];
/* Each slice's frontier then (bump and limit, NULL limits for a slice not
* opened yet), and the first address the worker carves in it. */
char *front_bump[SCR_SA_NCLASS], *front_lim[SCR_SA_NCLASS];
char *start[SCR_SA_NCLASS];
/* The worker's system blocks: an open-addressing set of `cap` slots (a
* power of two, or 0), NULL for an empty slot and SCR_SA_SYS_GONE for a
* removed entry; `used` counts both kinds of filled slots. */
void **sys;
size_t cap, used, live;
/* The blocks of the free lists set aside that the worker took once a slice
* had no bump room left, so that older free blocks are used before the
* slice grows; they go back to those lists with the worker's memory. */
ScrSaBlock **taken;
size_t ntaken, cap_taken;
bool shared;
bool poison;
};
static _Thread_local bool scr_sa_discarded; /* this thread's memory went back */
static ScrSaKeep *scr_sa_parked_keep[SCR_SA_ARENAS]; /* of discards that wait */
/* SCR_SA_DISCARD: 0 off, 1 on (the default), 2 poison. */
static int scr_sa_discard_mode(void) {
const char *mode = getenv("SCR_SA_DISCARD");
if (!mode) return 1;
if (strcmp(mode, "0") == 0) return 0;
return strcmp(mode, "poison") == 0 ? 2 : 1;
}
static void scr_sa_keep_begin(unsigned i) {
int mode = scr_sa_discard_mode();
if (!mode) return;
ScrSaKeep *k = calloc(1, sizeof *k);
if (!k) return;
k->poison = mode == 2;
for (unsigned c = 0; c < SCR_SA_NCLASS; c++) {
ScrSaHoles *h = &scr_sa_holes[i][c];
k->free[c] = scr_sa.free[c];
k->bump[c] = scr_sa.bump[c];
k->lim[c] = scr_sa.lim[c];
k->holes[c] = *h;
memset(h, 0, sizeof *h);
scr_sa.free[c] = NULL;
/* While the slice carves a returned region, its frontier is kept apart. */
if (k->holes[c].lim) {
scr_sa.bump[c] = k->holes[c].bump;
scr_sa.lim[c] = k->holes[c].lim;
}
k->front_bump[c] = scr_sa.bump[c];
k->front_lim[c] = scr_sa.lim[c];
k->start[c] = scr_sa.lim[c] ? scr_sa.bump[c] : (char *)scr_sa.base + ((size_t)c << scr_sa.shift);
}
scr_sa_keep = k;
}
/* A remote free of class c, drained while the worker keeps its memory
* apart: one of its own blocks makes it share; an older block goes to the
* free lists set aside. Once the worker has taken blocks from those lists,
* any older block might be one of them, so it shares as well. */
static void scr_sa_keep_drained(ScrSaKeep *k, ScrSaBlock *b, unsigned c) {
if ((char *)b >= k->start[c] && (char *)b < scr_sa.bump[c]) {
k->shared = true;
b->next = scr_sa.free[c];
scr_sa.free[c] = b;
} else {
if (k->ntaken) k->shared = true;
b->next = k->free[c];
k->free[c] = b;
}
}
/* A block of class c from the free lists set aside, for a worker whose
* slice has no bump room left; NULL when there is none or it cannot be
* recorded. */
static ScrSaBlock *scr_sa_keep_take(ScrSaKeep *k, unsigned c) {
ScrSaBlock *b = k->free[c];
if (!b) return NULL;
if (k->ntaken == k->cap_taken) {
size_t cap = k->cap_taken ? 2 * k->cap_taken : 256;
ScrSaBlock **taken = realloc(k->taken, cap * sizeof *taken);
if (!taken) return NULL;
k->taken = taken;
k->cap_taken = cap;
}
k->free[c] = b->next;
k->taken[k->ntaken++] = b;
return b;
}
static size_t scr_sa_sys_slot(const void *p, size_t cap) {
return (size_t)((((uint64_t)(uintptr_t)p >> 4) * UINT64_C(0x9E3779B97F4A7C15)) >> 32) & (cap - 1);
}
/* Records system block p. A worker whose set cannot grow keeps its memory. */
static void scr_sa_keep_add(ScrSaKeep *k, void *p) {
if ((k->used + 1) * 2 > k->cap) {
size_t cap = 64;
while (cap < (k->live + 1) * 4) cap *= 2;
void **sys = calloc(cap, sizeof *sys);
if (!sys) {
k->shared = true;
return;
}
for (size_t s = 0; s < k->cap; s++) {
void *q = k->sys[s];
if (!q || q == SCR_SA_SYS_GONE) continue;
size_t j = scr_sa_sys_slot(q, cap);
while (sys[j]) j = (j + 1) & (cap - 1);
sys[j] = q;
}
free(k->sys);
k->sys = sys;
k->cap = cap;
k->used = k->live;
}
size_t j = scr_sa_sys_slot(p, k->cap);
while (k->sys[j] && k->sys[j] != SCR_SA_SYS_GONE) j = (j + 1) & (k->cap - 1);
if (!k->sys[j]) k->used++;
k->sys[j] = p;
k->live++;
}
/* Forgets system block p; whether it was recorded. */
static bool scr_sa_keep_remove(ScrSaKeep *k, const void *p) {
if (!k->live) return false;
for (size_t j = scr_sa_sys_slot(p, k->cap); k->sys[j]; j = (j + 1) & (k->cap - 1)) {
if (k->sys[j] != p) continue;
k->sys[j] = SCR_SA_SYS_GONE;
k->live--;
return true;
}
return false;
}
/* The calling thread's arena keeps the worker's memory (it shared): the
* free lists set aside go back, followed by the worker's own free blocks,
* and so do the returned regions, past which the worker carved. */
static void scr_sa_keep_merge(ScrSaKeep *k) {
for (unsigned c = 0; c < SCR_SA_NCLASS; c++) {
ScrSaBlock *kept = k->free[c];
if (kept) {
ScrSaBlock *tail = kept;
while (tail->next) tail = tail->next;
tail->next = scr_sa.free[c];
scr_sa.free[c] = kept;
}
ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c];
*h = k->holes[c];
if (h->lim) {
h->bump = scr_sa.bump[c];
h->lim = scr_sa.lim[c];
scr_sa.bump[c] = k->bump[c];
scr_sa.lim[c] = k->lim[c];
}
}
free(k->sys);
free(k->taken);
free(k);
}
/* Returns the memory of the worker whose arena the calling thread (the
* trim helper) holds, and restores the arena to its state at the worker's
* claim; with poison, makes the pages inaccessible and moves the frontier
* past them instead. */
static void scr_sa_discard(ScrSaKeep *k) {
/* Remote frees since the worker's last drain. */
scr_sa_keep = k;
scr_sa_drain_remote();
scr_sa_keep = NULL;
if (k->shared) {
scr_sa_keep_merge(k);
scr_sa_trim();
return;
}
uintptr_t page = (uintptr_t)sysconf(_SC_PAGESIZE);
if (page == 0 || page == (uintptr_t)-1) page = 4096;
size_t returned = 0, sys_bytes = 0, sys_blocks = 0;
for (unsigned c = 0; c < SCR_SA_NCLASS; c++) {
char *lo = k->start[c], *top = scr_sa.bump[c], *lim = scr_sa.lim[c];
char *bump = k->front_bump[c], *limit = k->front_lim[c];
if (lim && top > lo) {
char *first = (char *)(((uintptr_t)lo + page - 1) & ~(page - 1));
char *used = (char *)(((uintptr_t)top + page - 1) & ~(page - 1));
/* The part of a page that may also hold older blocks. */
if (first > lo) memset(lo, 0, (size_t)((first < top ? first : top) - lo));
if (k->poison) {
if (used > first) (void)mprotect(first, (size_t)(used - first), PROT_NONE);
bump = used > first ? used : top;
limit = lim > bump ? lim : bump;
} else {
char *end = (char *)(((uintptr_t)lim + page - 1) & ~(page - 1));
if (end > first) {
(void)madvise(first, (size_t)(end - first), MADV_DONTNEED);
returned += (size_t)(end - first);
}
}
} else {
bump = top;
limit = lim;
}
scr_sa.free[c] = k->free[c];
ScrSaHoles *h = &scr_sa_holes[scr_sa_arena][c];
*h = k->holes[c];
if (h->lim) {
h->bump = bump;
h->lim = limit;
scr_sa.bump[c] = k->bump[c];
scr_sa.lim[c] = k->lim[c];
} else {
scr_sa.bump[c] = bump;
scr_sa.lim[c] = limit;
}
}
/* The older free blocks the worker took are free again. */
if (!k->poison)
for (size_t t = 0; t < k->ntaken; t++) {
ScrSaBlock *b = k->taken[t];
unsigned c = (unsigned)(((uintptr_t)b - scr_sa.base) >> scr_sa.shift);
b->next = scr_sa.free[c];
scr_sa.free[c] = b;
}
if (!k->poison)
for (size_t s = 0; s < k->cap; s++) {
void *p = k->sys[s];
if (!p || p == SCR_SA_SYS_GONE) continue;
#if defined(__GLIBC__)
sys_bytes += malloc_usable_size(p);
#endif
sys_blocks++;
free(p);
}
#if defined(__GLIBC__)
if (sys_blocks) (void)malloc_trim(0);
#endif
const char *stats = getenv("SCR_SA_STATS");
if (stats && stats[0] == '1')
fprintf(stderr, "scr_sa: discard arena %d returned %zu MB, %zu system blocks (%zu MB)\n", scr_sa_arena,
returned >> 20, sys_blocks, sys_bytes >> 20);
free(k->sys);
free(k->taken);
free(k);
}
/* Hooks of the allocator's system paths and of sharing (scr_rc.h). */
static void scr_sa_keep_note_system(void *p) {
if (p && scr_sa_keep) scr_sa_keep_add(scr_sa_keep, p);
}
void scr_sa_thread_shares(void) {
if (scr_sa_keep) scr_sa_keep->shared = true;
}
/* Worker threads that started and have not parked. */
static _Atomic(unsigned) scr_sa_workers_live;
/* Library archives run on their embedder's threads and keep their memory. */
static void scr_sa_worker_starting(void) {
#ifndef SCR_LIB
scr_sa_worker = true;
atomic_fetch_add_explicit(&scr_sa_workers_live, 1, memory_order_relaxed);
#endif
}
#define SCR_SA_WORKER_STARTING() scr_sa_worker_starting()
#endif
#ifdef SCR_SA_TRIM
@@ -912,6 +1243,85 @@ static void *scr_sa_trim_main(void *arg) {
return NULL;
}
/* Hands the calling thread's arena to a trim helper; false if no helper
* could start. */
static bool scr_sa_start_helper(unsigned i) {
scr_sa_parked[i] = scr_sa;
atomic_fetch_add_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
pthread_attr_t attr;
pthread_t helper;
bool started = pthread_attr_init(&attr) == 0;
if (started) {
started = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 &&
pthread_create(&helper, &attr, scr_sa_trim_main, (void *)(uintptr_t)i) == 0;
pthread_attr_destroy(&attr);
}
if (!started) atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
return started;
}
/* Discards that wait for the last running worker: one bit per arena, whose
* state and ScrSaKeep are in scr_sa_parked and scr_sa_parked_keep. */
static pthread_mutex_t scr_sa_pending_lock = PTHREAD_MUTEX_INITIALIZER;
static uint32_t scr_sa_pending;
/* Queues the discard of the calling worker's arena i; the arena stays
* claimed until it is done. */
static void scr_sa_defer_discard(unsigned i, ScrSaKeep *k) {
atomic_fetch_add_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
pthread_mutex_lock(&scr_sa_pending_lock);
scr_sa_parked[i] = scr_sa;
scr_sa_parked_keep[i] = k;
scr_sa_pending |= 1u << i;
pthread_mutex_unlock(&scr_sa_pending_lock);
}
/* Runs the waiting discards on the calling thread, which ends with no arena;
* returns whether there were any. */
static bool scr_sa_drain(void) {
bool any = false;
for (;;) {
pthread_mutex_lock(&scr_sa_pending_lock);
uint32_t pending = scr_sa_pending;
unsigned i = pending ? (unsigned)__builtin_ctz(pending) : 0;
scr_sa_pending &= ~(1u << i);
pthread_mutex_unlock(&scr_sa_pending_lock);
if (!pending) return any;
any = true;
scr_sa_arena = (int)i;
scr_sa = scr_sa_parked[i];
ScrSaKeep *k = scr_sa_parked_keep[i];
scr_sa_parked_keep[i] = NULL;
scr_sa_discard(k);
scr_sa_release(i);
atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_release);
}
}
static void *scr_sa_drain_main(void *arg) {
(void)arg;
scr_sa_drain();
return NULL;
}
/* A worker parked: the last one running starts the waiting discards. */
static void scr_sa_worker_parked(void) {
if (atomic_fetch_sub_explicit(&scr_sa_workers_live, 1, memory_order_acq_rel) != 1) return;
pthread_mutex_lock(&scr_sa_pending_lock);
bool pending = scr_sa_pending != 0;
pthread_mutex_unlock(&scr_sa_pending_lock);
if (!pending) return;
pthread_attr_t attr;
pthread_t helper;
bool started = pthread_attr_init(&attr) == 0;
if (started) {
started = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 &&
pthread_create(&helper, &attr, scr_sa_drain_main, NULL) == 0;
pthread_attr_destroy(&attr);
}
if (!started) scr_sa_drain();
}
void scr_sa_wait_trims(void) {
while (atomic_load_explicit(&scr_sa_trimming, memory_order_acquire)) {
struct timespec pause = {0, 1000000};
@@ -920,27 +1330,45 @@ void scr_sa_wait_trims(void) {
}
#else
void scr_sa_wait_trims(void) {}
void scr_sa_thread_shares(void) {}
#endif
static void scr_sa_park_arena(void);
void scr_sa_thread_park(void) {
#ifdef SCR_SA_TRIM
bool worker = scr_sa_worker;
scr_sa_worker = false;
scr_sa_park_arena();
if (worker) scr_sa_worker_parked();
#else
scr_sa_park_arena();
#endif
}
static void scr_sa_park_arena(void) {
if (scr_sa_arena < 0) return;
unsigned i = (unsigned)scr_sa_arena;
#if defined(SCR_SA_PREFAULT) && defined(SCR_WORKERS)
scr_sa_pf_park();
#endif
#ifdef SCR_SA_TRIM
ScrSaKeep *k = scr_sa_keep;
scr_sa_keep = NULL;
if (k && !k->shared) {
/* Nothing this thread allocated is reachable from another one: its
* memory goes back once no other worker runs (scr_sa_worker_parked),
* and later frees on this thread are dropped. */
scr_sa_discarded = true;
scr_sa_defer_discard(i, k);
memset(&scr_sa, 0, sizeof scr_sa);
scr_sa_arena = -1;
scr_sa_refused = true;
return;
}
if (k) scr_sa_keep_merge(k);
if (scr_sa_trim_worth()) {
scr_sa_parked[i] = scr_sa;
atomic_fetch_add_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
pthread_attr_t attr;
pthread_t helper;
bool started = pthread_attr_init(&attr) == 0;
if (started) {
started = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) == 0 &&
pthread_create(&helper, &attr, scr_sa_trim_main, (void *)(uintptr_t)i) == 0;
pthread_attr_destroy(&attr);
}
if (started) {
if (scr_sa_start_helper(i)) {
/* Later frees on this thread take the foreign path into the arena's
* remote list; the helper releases the claim. */
memset(&scr_sa, 0, sizeof scr_sa);
@@ -948,7 +1376,6 @@ void scr_sa_thread_park(void) {
scr_sa_refused = true;
return;
}
atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
scr_sa_trim();
}
#endif
@@ -971,6 +1398,9 @@ static size_t scr_sa_foreign_size(const void *p) {
}
void scr_sa_free_foreign(void *p) {
#ifdef SCR_SA_TRIM
if (scr_sa_discarded) return; /* the block went back with the thread's memory */
#endif
uintptr_t region = atomic_load_explicit(&scr_sa_region, memory_order_acquire);
uintptr_t off = (uintptr_t)p - region;
if (region && off < scr_sa_arena_span * SCR_SA_ARENAS) {
@@ -982,6 +1412,9 @@ void scr_sa_free_foreign(void *p) {
memory_order_relaxed));
return;
}
#ifdef SCR_SA_TRIM
if (scr_sa_keep) (void)scr_sa_keep_remove(scr_sa_keep, p);
#endif
free(p);
}
#else
@@ -1020,6 +1453,15 @@ __attribute__((cold, noinline)) void *scr_sa_slow(size_t n, bool zero) {
if (zero) memset(b, 0, n);
return b;
}
#ifdef SCR_SA_TRIM
/* A worker that keeps its memory apart uses the arena's older free
* blocks before its slice grows. */
if (scr_sa_keep && (size_t)(scr_sa.lim[c] - scr_sa.bump[c]) < scr_sa_class_size(c) &&
(b = scr_sa_keep_take(scr_sa_keep, c)) != NULL) {
if (zero) memset(b, 0, n);
return b;
}
#endif
#endif
/* Carve from the slice, opening it or raising its limit as needed;
* fresh pages are zero. */
@@ -1051,6 +1493,9 @@ __attribute__((cold, noinline)) void *scr_sa_slow(size_t n, bool zero) {
SCR_SA_LARGE_NOTE(q, n);
SCR_SA_THREAD_LARGE_NOTE();
}
#ifdef SCR_SA_TRIM
scr_sa_keep_note_system(q);
#endif
return q;
}
@@ -1065,8 +1510,14 @@ void *scr_mem_realloc(void *p, size_t n) {
}
#endif
else {
#ifdef SCR_SA_TRIM
bool kept = scr_sa_keep && scr_sa_keep_remove(scr_sa_keep, p);
#endif
void *q = realloc(p, n);
if (n > SCR_SA_MAX) SCR_SA_LARGE_NOTE(q, n);
#ifdef SCR_SA_TRIM
if (kept) scr_sa_keep_note_system(q ? q : p); /* a failed realloc leaves p */
#endif
return q;
}
/* Still fits its block, which is aligned for n (see the contract). */
@@ -1082,10 +1533,15 @@ void *scr_mem_realloc(void *p, size_t n) { return realloc(p, n); }
#ifdef SCR_WORKERS
void scr_sa_thread_park(void) {}
void scr_sa_wait_trims(void) {}
void scr_sa_thread_shares(void) {}
void scr_sa_free_foreign(void *p) { free(p); }
#endif
#endif
#ifndef SCR_SA_WORKER_STARTING
#define SCR_SA_WORKER_STARTING() ((void)0)
#endif
#ifdef SCR_WORKERS
/* Blocks above SCR_SA_MAX come from glibc, which gives each thread that
* allocates its own arena: a heap reserved up front and made usable with
@@ -1116,7 +1572,10 @@ void scr_sa_free_foreign(void *p) { free(p); }
* that are not workers (they never reserve). */
static _Thread_local unsigned scr_sa_thread_large;
void scr_sa_thread_starting(void) { scr_sa_thread_large = 1; }
void scr_sa_thread_starting(void) {
scr_sa_thread_large = 1;
SCR_SA_WORKER_STARTING();
}
static void scr_sa_thread_reserve(void) {
void *blocks[SCR_SA_THREAD_HEAP_RESERVE / SCR_SA_THREAD_HEAP_STEP + 1];
@@ -1136,7 +1595,7 @@ static void scr_sa_thread_large_note(void) {
if (count == SCR_SA_THREAD_RESERVE_AFTER) scr_sa_thread_reserve();
}
#else
void scr_sa_thread_starting(void) {}
void scr_sa_thread_starting(void) { SCR_SA_WORKER_STARTING(); }
#endif
#endif
+327
View File
@@ -429,6 +429,332 @@ static void check_trim(void) {
}
#endif
#if defined(SCR_WORKERS) && SCR_SMALL_ALLOC && defined(__linux__)
#include <malloc.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* A worker thread (scr_sa_thread_starting) that exits without sharing
* gives its memory back: the pages it carved, and the system blocks it
* still holds. Older blocks of the arena, and the memory of a worker that
* shared (or got a remote free of its own blocks), stay. */
/* DISCARD_SIZE and POISON_SIZE are classes the earlier checks leave unused
* in the workers' arena, so their state there is known. */
enum { DISCARD_BLOCKS = 1 << 20, DISCARD_SIZE = 40, POISON_SIZE = 56, DISCARD_OLD = 64, DISCARD_LARGE = 64 };
enum { DISCARD_LARGE_SIZE = 64 << 10 };
static unsigned char *old_blocks[DISCARD_OLD];
static unsigned char **discard_blocks;
static unsigned char *discard_large[DISCARD_LARGE];
static uintptr_t discard_base, discard_lo, discard_hi;
static size_t block_size = DISCARD_SIZE; /* of the workers' and reusing threads' blocks */
/* Whether discard workers claim the arena of discard_old_owner (no other worker holds it). */
static bool discard_in_old_arena = true;
/* The end of the highest block of class DISCARD_SIZE so far, and how many blocks of a run lay past the mark
* at its start: those the slice grew for. */
static uintptr_t class_hi;
static size_t discard_beyond, reuse_beyond;
static bool discard_shares;
static atomic_int discard_step;
static void wait_step(int step) {
while (atomic_load(&discard_step) < step) {
struct timespec pause = {0, 100000};
nanosleep(&pause, NULL);
}
}
/* A plain script thread whose blocks stay live in its parked arena. */
static void *discard_old_owner(void *arg) {
(void)arg;
for (unsigned i = 0; i < DISCARD_OLD; i++) {
old_blocks[i] = scr_mem_alloc(DISCARD_SIZE);
assert(owned(old_blocks[i]));
fill(old_blocks[i], DISCARD_SIZE, (unsigned char)i);
}
discard_base = scr_sa.base;
scr_sa_thread_park();
return NULL;
}
static void allocate_discardable(void) {
discard_lo = UINTPTR_MAX;
discard_hi = 0;
discard_beyond = 0;
for (size_t i = 0; i < DISCARD_BLOCKS; i++) {
unsigned char *p = scr_mem_alloc(block_size);
assert(p && owned(p));
fill(p, block_size, (unsigned char)i);
discard_blocks[i] = p;
if ((uintptr_t)p >= class_hi) discard_beyond++;
if ((uintptr_t)p < discard_lo) discard_lo = (uintptr_t)p;
if ((uintptr_t)p + block_size > discard_hi) discard_hi = (uintptr_t)p + block_size;
}
if (block_size == DISCARD_SIZE && discard_hi > class_hi) class_hi = discard_hi;
/* System blocks: some freed, some moved by realloc, the rest kept. */
for (unsigned i = 0; i < DISCARD_LARGE; i++) {
discard_large[i] = scr_mem_alloc(DISCARD_LARGE_SIZE);
assert(discard_large[i] && !owned(discard_large[i]));
fill(discard_large[i], 64, (unsigned char)i);
}
for (unsigned i = 0; i < DISCARD_LARGE; i += 4) {
scr_mem_free(discard_large[i]);
discard_large[i] = NULL;
}
for (unsigned i = 1; i < DISCARD_LARGE; i += 4) discard_large[i] = scr_mem_realloc(discard_large[i], 2 * DISCARD_LARGE_SIZE);
}
static void *discard_worker(void *arg) {
(void)arg;
scr_sa_thread_starting();
allocate_discardable();
/* None of the arena's older blocks or free blocks is reused. */
if (scr_sa.base == discard_base)
for (unsigned i = 0; i < DISCARD_OLD; i++) assert(discard_lo > (uintptr_t)old_blocks[i]);
else assert(!discard_in_old_arena);
if (discard_shares) scr_sa_thread_shares();
scr_sa_thread_park();
return NULL;
}
/* A worker one of whose blocks the main thread frees while it runs. */
static void *remote_freed_worker(void *arg) {
(void)arg;
scr_sa_thread_starting();
allocate_discardable();
atomic_store(&discard_step, 1);
wait_step(2);
/* A class this thread has not used: its first block takes the slow path,
* which drains the remote free. */
void *p = scr_mem_alloc(200);
assert(p);
scr_sa_thread_park();
return NULL;
}
/* A plain thread that takes as many blocks again from the arena: carved
* ones must be zero (the fast path does not clear them). */
static size_t reuse_outside;
static uintptr_t reuse_hi;
static void *discard_reuse(void *arg) {
(void)arg;
reuse_outside = 0;
reuse_hi = 0;
reuse_beyond = 0;
for (size_t i = 0; i < DISCARD_BLOCKS; i++) {
unsigned char *p = scr_mem_calloc(block_size);
assert(p && owned(p));
assert(scr_sa.base == discard_base);
for (size_t k = 0; k < block_size; k++) assert(p[k] == 0);
for (unsigned j = 0; j < DISCARD_OLD; j++) assert(p != old_blocks[j]);
if ((uintptr_t)p < discard_lo || (uintptr_t)p >= discard_hi) reuse_outside++;
if ((uintptr_t)p + block_size > reuse_hi) reuse_hi = (uintptr_t)p + block_size;
if ((uintptr_t)p >= class_hi) reuse_beyond++;
discard_blocks[i] = p;
}
if (block_size == DISCARD_SIZE && reuse_hi > class_hi) class_hi = reuse_hi;
for (size_t i = 0; i < DISCARD_BLOCKS; i++) scr_mem_free(discard_blocks[i]);
scr_sa_thread_park();
return NULL;
}
static void run(void *(*fn)(void *)) {
pthread_t t;
assert(pthread_create(&t, NULL, fn, NULL) == 0);
assert(pthread_join(t, NULL) == 0);
scr_sa_wait_trims();
}
static void verify_old_blocks(void) {
for (unsigned i = 0; i < DISCARD_OLD; i++) verify(old_blocks[i], DISCARD_SIZE, (unsigned char)i);
}
/* The worker's blocks stayed as it left them, and the next thread to claim
* the arena carves past them; then they are freed (remote frees). */
static void verify_kept(void) {
for (size_t i = 0; i < DISCARD_BLOCKS; i++) verify(discard_blocks[i], block_size, (unsigned char)i);
for (unsigned i = 0; i < DISCARD_LARGE; i++) {
if (!discard_large[i]) continue;
verify(discard_large[i], 64, (unsigned char)i);
free(discard_large[i]);
}
for (size_t i = 0; i < DISCARD_BLOCKS; i++) scr_mem_free(discard_blocks[i]);
}
/* The protection of the mapping that holds address a, from /proc/self/maps. */
static bool readable(uintptr_t a) {
FILE *maps = fopen("/proc/self/maps", "r");
assert(maps);
char line[512];
bool found = false, result = false;
while (!found && fgets(line, sizeof line, maps)) {
unsigned long lo, hi;
char perms[8];
if (sscanf(line, "%lx-%lx %7s", &lo, &hi, perms) != 3) continue;
if (a >= lo && a < hi) {
found = true;
result = perms[0] == 'r';
}
}
fclose(maps);
assert(found);
return result;
}
/* A worker that runs until discard_step reaches 3. */
static void *holding_worker(void *arg) {
(void)arg;
scr_sa_thread_starting();
void *p = scr_mem_alloc(DISCARD_SIZE);
assert(p && owned(p));
atomic_store(&discard_step, 1);
wait_step(3);
scr_sa_thread_park();
return NULL;
}
/* A worker that allocates one block of a class the other checks leave
* alone and parks without sharing. */
static void *small_worker(void *arg) {
(void)arg;
scr_sa_thread_starting();
void *p = scr_mem_alloc(200);
assert(p && owned(p));
scr_sa_thread_park();
return NULL;
}
/* Discards wait while another worker runs, and the last worker to park
* starts them. When every arena is held by a discard that waits, a worker
* that needs one runs them itself. */
static void check_deferred_discard(void) {
size_t page = (size_t)sysconf(_SC_PAGESIZE);
atomic_store(&discard_step, 0);
pthread_t holder;
assert(pthread_create(&holder, NULL, holding_worker, NULL) == 0);
wait_step(1);
discard_shares = false;
discard_in_old_arena = false;
pthread_t t;
assert(pthread_create(&t, NULL, discard_worker, NULL) == 0);
assert(pthread_join(t, NULL) == 0);
uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1);
size_t total = (discard_hi - first) / page;
/* Still resident (the arena is not reused meanwhile, so the blocks are intact). */
assert(resident_pages(first, discard_hi) > total / 2);
for (size_t i = 0; i < DISCARD_BLOCKS; i += 4099) verify(discard_blocks[i], block_size, (unsigned char)i);
/* More arenas held by waiting discards than there are free ones. */
for (unsigned n = 0; n < 40; n++) {
assert(pthread_create(&t, NULL, small_worker, NULL) == 0);
assert(pthread_join(t, NULL) == 0);
}
atomic_store(&discard_step, 3);
assert(pthread_join(holder, NULL) == 0);
scr_sa_wait_trims();
assert(resident_pages(first, discard_hi) < total / 2);
discard_in_old_arena = true;
}
static void check_discard(void) {
discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks);
assert(discard_blocks);
run(discard_old_owner);
size_t page = (size_t)sysconf(_SC_PAGESIZE);
/* Discarded: its pages are returned, and carved again (zeroed) by the next
* thread, which does not grow the slice. The arena's older blocks stay. */
discard_shares = false;
run(discard_worker);
verify_old_blocks();
uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1);
/* Pre-fault requests still in flight may fault in a few megabytes again. */
size_t total = (discard_hi - first) / page;
assert(resident_pages(first, discard_hi) < total / 2);
run(discard_reuse);
assert(reuse_outside < 256);
verify_old_blocks();
/* SCR_SA_DISCARD=poison makes the pages inaccessible; the next thread
* carves past them. */
block_size = POISON_SIZE;
setenv("SCR_SA_DISCARD", "poison", 1);
run(discard_worker);
unsetenv("SCR_SA_DISCARD");
verify_old_blocks();
first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1);
assert(!readable(first));
assert(!readable((discard_hi - 1) & ~(uintptr_t)(page - 1)));
run(discard_reuse);
assert(reuse_outside == DISCARD_BLOCKS);
verify_old_blocks();
block_size = DISCARD_SIZE;
/* A worker that shared keeps its memory. */
discard_shares = true;
run(discard_worker);
verify_old_blocks();
verify_kept();
/* So does one that got a remote free of one of its blocks. */
atomic_store(&discard_step, 0);
pthread_t t;
assert(pthread_create(&t, NULL, remote_freed_worker, NULL) == 0);
wait_step(1);
scr_mem_free(discard_blocks[0]);
discard_blocks[0] = scr_mem_alloc(DISCARD_SIZE); /* main's own block, for verify_kept */
fill(discard_blocks[0], DISCARD_SIZE, 0);
atomic_store(&discard_step, 2);
assert(pthread_join(t, NULL) == 0);
scr_sa_wait_trims();
verify_old_blocks();
verify_kept();
/* SCR_SA_DISCARD=0 keeps every worker's memory. */
discard_shares = false;
setenv("SCR_SA_DISCARD", "0", 1);
run(discard_worker);
unsetenv("SCR_SA_DISCARD");
verify_old_blocks();
verify_kept();
/* The kept workers' blocks are free now (remote frees): a worker takes
* them once its slice has no bump room left, and they are free again
* after its memory goes back, so the next thread does not grow the slice
* either. Past the highest block so far, each carves at most the bump room
* the slice had left (a few megabytes). */
size_t room = ((size_t)4 << 20) / DISCARD_SIZE;
run(discard_worker);
verify_old_blocks();
assert(discard_beyond < room);
run(discard_reuse);
assert(reuse_beyond < room);
verify_old_blocks();
/* Returned regions (holes) of an earlier trim are kept apart while a
* worker runs, and carved again afterwards. */
trim_blocks = malloc(TRIM_BLOCKS * sizeof *trim_blocks); /* check_trim freed its own */
assert(trim_blocks);
run(trim_allocate);
uintptr_t hole_lo = trim_lo, hole_hi = trim_hi;
discard_shares = false;
run(discard_worker);
verify_old_blocks();
for (size_t k = 0; k < TRIM_BLOCKS / TRIM_KEEP; k++)
verify(trim_kept[k], TRIM_SIZE, (unsigned char)(k * TRIM_KEEP));
trim_lo = hole_lo;
trim_hi = hole_hi;
run(trim_reuse);
assert(trim_outside < 4 * TRIM_BLOCKS / TRIM_KEEP + 256);
for (size_t k = 0; k < TRIM_BLOCKS / TRIM_KEEP; k++) scr_mem_free(trim_kept[k]);
free(trim_blocks);
check_deferred_discard();
for (unsigned i = 0; i < DISCARD_OLD; i++) scr_mem_free(old_blocks[i]);
free(discard_blocks);
}
#endif
int main(void) {
#if SCR_SMALL_ALLOC
check_slice_growth(); /* first: its class must be untouched */
@@ -446,6 +772,7 @@ int main(void) {
check_threads();
#ifdef __linux__
check_trim();
check_discard();
#endif
printf("allocator checks passed: size-class workers\n");
#else
+3
View File
@@ -994,6 +994,9 @@ double scr_set_interval(ScrClosure *cb, double ms) {
return (double)id;
}
SCR_TL void (*scr_mt_abort_timeout_arm)(ScrClosure *cb, double ms) = NULL;
SCR_TL int scr_mt_wait_state = 0;
double scr_set_timeout_handle(ScrClosure *cb, double ms) {
ms = scr_timer_coerce_ms(ms);
unsigned long id = scr_interval_next_id++;
+8
View File
@@ -367,6 +367,14 @@ ScrPromise *scr_immediate_promise(void);
void scr_clear_interval(double handle);
double scr_set_timeout_handle(ScrClosure *cb, double ms); /* cb ownership moves in */
/* Queues `cb` (moved) on the MockTimers that mocks AbortSignal.timeout
* (scr_test.c), NULL while it is not mocked. AbortSignal.timeout then
* aborts its signal from that queue instead of arming a loop timer. */
extern SCR_TL void (*scr_mt_abort_timeout_arm)(ScrClosure *cb, double ms);
/* scheduler.wait's mock.timers state (scr_test.c): 0 real, 1 mocked, 2 the
* real one restored after a reset, which throws ERR_INVALID_THIS in Node.
* scr_tp_scheduler_wait_signal reads it. */
extern SCR_TL int scr_mt_wait_state;
void scr_timers_teardown(void);
/* Timeout.unref()/ref()/hasRef(): loop-liveness bookkeeping over the timer
* handle (setTimeout/setInterval's id). unref drops the timer from the
+93 -7
View File
@@ -94,6 +94,25 @@
* objects that are not garbage. A pass that frees almost
* nothing therefore doubles this fraction, and a productive
* one restores it.
* full, death live objects have died outside passes since the last
* full pass (the live count fell between passes, by at
* least 1/32 of the heap that pass left) and have stopped
* dying, and the heap has grown the base fraction since.
* Growth alone cannot tell a heap that keeps live data
* from one that keeps garbage, and once full passes stop
* finding anything the fraction widens to the cap. A
* program that drops a large structure and builds another
* (a compiler rebuilding in watch mode) would then hold
* the dead structures' cyclic parts until the heap had
* grown to the cap. The acyclic parts of such a structure
* go by reference counting, which shows as the fall. One
* that finds fewer older objects than had died doubles the
* fall the next one needs, and a productive one restores
* it. Like every full pass the growth rule did not choose
* (backlog, scheduled age), it can raise the levels'
* growth baselines but not lower them: the heap regrowing
* to where it was before such a pass reclaimed a dropped
* structure is a rebuild, not growth.
* mature the same two rules one level down: a growth fraction
* measured from the last mature-or-full pass and scheduled
* by everything mature passes free, and a backlog rule over
@@ -196,6 +215,12 @@ typedef struct {
unsigned char growth_shift[SCR_CYC_NGENS];
unsigned char growth_streak[SCR_CYC_NGENS];
unsigned backlog_shift[SCR_CYC_NGENS];
size_t fallen;
size_t live_seen;
size_t full_after;
unsigned char death_shift;
bool falling;
bool keep_base;
size_t nbuffered;
size_t trigger;
size_t alloc_trigger;
@@ -234,6 +259,12 @@ static _Thread_local ScrCycThread scr_cyc_thread = {.gen_limit = SCR_CYC_OLD};
#define scr_cyc_scheduled_mature_age (scr_cyc_thread.scheduled_mature_age)
#define scr_xg_freed (scr_cyc_thread.xg_freed)
#define scr_cyc_mature_dead (scr_cyc_thread.mature_dead)
#define scr_cyc_fallen (scr_cyc_thread.fallen)
#define scr_cyc_live_seen (scr_cyc_thread.live_seen)
#define scr_cyc_full_after (scr_cyc_thread.full_after)
#define scr_cyc_death_shift (scr_cyc_thread.death_shift)
#define scr_cyc_falling (scr_cyc_thread.falling)
#define scr_cyc_keep_base (scr_cyc_thread.keep_base)
#define scr_cyc_kinds (scr_cyc_thread.kinds)
#define scr_cyc_kinds_cap (scr_cyc_thread.kinds_cap)
#endif
@@ -475,6 +506,22 @@ static SCR_TL unsigned scr_cyc_backlog_shift[SCR_CYC_NGENS];
static SCR_TL size_t scr_cyc_mature_dead = 0;
#endif
/* The death rule (see the scheduling notes above). `fallen`: how far the
* live count fell between passes since the last full pass; `live_seen`: the
* live count at the end of the last pass or check; `full_after`: the live
* count after the last full pass; `falling`: whether it fell since the
* previous check; `death_shift`: doublings of the fall a pass needs, earned
* by passes that found less than had died; `keep_base`: the pass running
* is a full pass the growth rule did not choose, which may raise the
* levels' growth baselines but not lower them. */
#define SCR_CYC_DEATH_DIV 32
#define SCR_CYC_DEATH_MAX_SHIFT 8
#ifndef SCR_WORKERS
static SCR_TL size_t scr_cyc_fallen = 0, scr_cyc_live_seen = 0, scr_cyc_full_after = 0;
static SCR_TL unsigned char scr_cyc_death_shift = 0;
static SCR_TL bool scr_cyc_falling = false, scr_cyc_keep_base = false;
#endif
static size_t scr_cyc_nursery_threshold(void) {
static SCR_TL size_t cached = 0;
if (cached == 0) {
@@ -621,6 +668,27 @@ static void scr_cyc_adapt_growth(unsigned gen, size_t growth,
}
}
/* How far the live count fell since the last check: what died outside
* passes, less what was allocated meanwhile. Every pass ends with a fresh
* reading, so what passes free does not count. */
static void scr_cyc_note_fall(void) {
scr_cyc_falling = scr_cyc_live < scr_cyc_live_seen;
if (scr_cyc_falling) scr_cyc_fallen += scr_cyc_live_seen - scr_cyc_live;
scr_cyc_live_seen = scr_cyc_live;
}
/* The death rule: enough has died since the last full pass, it has stopped
* dying (a structure torn down by reference counting falls over several
* checks, and a pass in the middle would find its rest still referenced),
* and the heap has grown the base fraction since. */
static SCR_CYC_SCHEDULING bool scr_cyc_died(void) {
if (scr_cyc_falling) return false;
size_t base = scr_cyc_growth_base(scr_cyc_full_after);
size_t need = base / SCR_CYC_DEATH_DIV;
need = need > (SIZE_MAX >> scr_cyc_death_shift) ? SIZE_MAX : need << scr_cyc_death_shift;
return scr_cyc_fallen >= need && scr_cyc_live > base + base / SCR_CYC_GROWTH_DIV;
}
/* One scheduled pass at `gen`. A mature or old pass adapts that level's
* backlog threshold to its yield; the explicit sweep (scr_collect_cycles) is
* not a scheduling decision and leaves the thresholds alone. Productive means
@@ -630,11 +698,14 @@ static void scr_cyc_adapt_growth(unsigned gen, size_t growth,
*
* Every level shares this one body, so a program that links the collector
* carries a single copy of the scheduling logic. */
static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_growth) {
static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_growth, bool by_death) {
size_t base = scr_cyc_growth_base(scr_cyc_live_after[gen]);
size_t growth = scr_cyc_live > base ? scr_cyc_live - base : 0;
size_t fallen = scr_cyc_fallen;
size_t old_freed_before = scr_cyc_old_freed;
scr_cyc_keep_base = gen == SCR_CYC_OLD && !by_growth;
size_t freed = scr_cyc_pass(gen);
scr_cyc_keep_base = false;
size_t old_reclaimed = scr_cyc_old_freed - old_freed_before + scr_cyc_mature_dead;
if (gen == SCR_CYC_NURSERY) return;
if (scr_cands.n != 0) {
@@ -643,7 +714,10 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow
else if (scr_cyc_backlog_shift[gen] < SCR_CYC_BACKLOG_MAX_SHIFT)
scr_cyc_backlog_shift[gen]++;
}
if (by_growth)
if (by_death) {
if (old_reclaimed >= fallen) scr_cyc_death_shift = 0;
else if (scr_cyc_death_shift < SCR_CYC_DEATH_MAX_SHIFT) scr_cyc_death_shift++;
} else if (by_growth)
scr_cyc_adapt_growth(gen, growth,
gen == SCR_CYC_OLD ? old_reclaimed : freed);
}
@@ -652,15 +726,17 @@ static SCR_CYC_SCHEDULING void scr_cyc_scheduled_pass(unsigned gen, bool by_grow
* per pass, so it is kept out of line and built for size, which leaves the
* hot buffering path in scr_cyc_on_release small. */
static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) {
scr_cyc_note_fall();
unsigned gen = SCR_CYC_OLD;
bool by_growth = scr_cyc_grown(SCR_CYC_OLD);
if (!by_growth && scr_roots[SCR_CYC_OLD].n < scr_cyc_backlog_threshold(SCR_CYC_OLD)) {
bool by_death = !by_growth && scr_cyc_died();
if (!by_growth && !by_death && scr_roots[SCR_CYC_OLD].n < scr_cyc_backlog_threshold(SCR_CYC_OLD)) {
gen = SCR_CYC_MATURE;
by_growth = scr_cyc_grown(SCR_CYC_MATURE);
if (!by_growth && scr_roots[SCR_CYC_MATURE].n < scr_cyc_backlog_threshold(SCR_CYC_MATURE))
gen = SCR_CYC_NURSERY;
}
scr_cyc_scheduled_pass(gen, by_growth);
scr_cyc_scheduled_pass(gen, by_growth, by_death);
}
/* One scheduled pass, for callers that reach a natural collection point
@@ -669,6 +745,7 @@ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) {
* per turn would walk the whole live heap every turn — which is exactly the
* cost the generations exist to avoid. */
void scr_cyc_collect_scheduled(void) {
scr_cyc_note_fall();
if (scr_cyc_buffered() == 0) {
scr_cyc_scheduled_mature_age = 0;
return;
@@ -677,7 +754,7 @@ void scr_cyc_collect_scheduled(void) {
scr_cyc_scheduled_mature_age = 0;
} else if (++scr_cyc_scheduled_mature_age
>= scr_cyc_nursery_threshold()) {
scr_cyc_scheduled_pass(SCR_CYC_OLD, false);
scr_cyc_scheduled_pass(SCR_CYC_OLD, false, false);
return;
}
scr_cyc_collect_due();
@@ -1167,11 +1244,20 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
/* A limited pass walked only some of the candidates: the schedule's
* baselines stay those of the last pass over them all. */
scr_cyc_live_seen = scr_cyc_live;
if (scr_cyc_take == NULL) {
if (gen_limit >= SCR_CYC_MATURE) scr_cyc_live_after[SCR_CYC_MATURE] = scr_cyc_live;
/* A full pass the growth rule did not choose (death, backlog, scheduled
* age) may raise the growth baselines but not lower them. After it
* reclaims a dropped structure, the heap regrowing to where it was is
* the program rebuilding what it had, not growth: from the low point,
* the next growth passes would walk the rebuilt live heap for nothing,
* at both levels. */
for (unsigned g = SCR_CYC_MATURE; g <= gen_limit; g++)
if (!scr_cyc_keep_base || scr_cyc_live > scr_cyc_live_after[g]) scr_cyc_live_after[g] = scr_cyc_live;
if (gen_limit >= SCR_CYC_OLD) {
scr_cyc_live_after[SCR_CYC_OLD] = scr_cyc_live;
scr_cyc_scheduled_mature_age = 0;
scr_cyc_fallen = 0;
scr_cyc_full_after = scr_cyc_live;
}
}
/* markRoots drained buffers in bulk and teardowns moved the count around;
+13
View File
@@ -600,6 +600,7 @@ ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value);
bool scr_dyn_native_search_params_is(const ScrDyn *value);
bool scr_dyn_native_date_is(const ScrDyn *value);
ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments);
ScrDyn *scr_dyn_native_date_from_ms(double ms);
double scr_dyn_native_date_value(const ScrDyn *value);
ScrDyn *scr_dyn_native_regex(ScrRegex *value);
bool scr_dyn_native_regex_is(const ScrDyn *value);
@@ -669,6 +670,10 @@ ScrDyn *scr_dyn_from_iterator(const ScrDyn *value, int *kind);
ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len);
void scr_dyn_install_iterator_symbol(ScrDyn *key);
void scr_dyn_install_async_iterator_symbol(ScrDyn *key);
void scr_dyn_install_async_dispose_symbol(ScrDyn *key);
/* True when key is Symbol.asyncDispose (registered by the symbol unit when
* the program names it); handle symbol_get ops outside that unit use it. */
bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key);
ScrDyn *scr_dyn_array_values_function(void);
ScrDyn *scr_dyn_array_from_iterator(const ScrDyn *value);
ScrDyn *scr_dyn_iterator_result(ScrDyn *value);
@@ -890,6 +895,7 @@ bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right);
ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method);
ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, const ScrStr *what);
ScrDyn *scr_dyn_value_of(const ScrDyn *d, const ScrStr *what);
ScrStr *scr_dyn_error_string(const ScrDyn *d);
ScrStr *scr_num_to_string_radix(double value, const ScrDyn *radix);
/* JS String() over the dyn kind (units render "null"/"undefined" where
* scr_dyn_to_string throws) — the web globals' WebIDL ToString. +1. */
@@ -956,6 +962,13 @@ ScrDyn *scr_structured_clone_transfer_fail_with(const ScrDyn *first);
/* Validates a structuredClone options dyn value (throws on failure —
* callers must check scr_exc_pending). Borrowed, NULL-tolerant. */
void scr_sc_validate_options(const ScrDyn *options);
/* markAsUncloneable(value) and the check structuredClone and postMessage
* make (scr_json.c); both borrow the value. scr_uncloneable_count is the
* number of marked values, so the clone paths test it inline and call
* scr_uncloneable_marked only once something is marked. */
void scr_mark_uncloneable(ScrDyn *value);
bool scr_uncloneable_marked(const ScrDyn *value);
extern SCR_TL size_t scr_uncloneable_count;
/* An %Error as the boundary's dyn shape ({name, message[, code]}).
* Borrows; +1. */
ScrDyn *scr_dyn_from_error(const ScrError *e);
+4 -7
View File
@@ -82,13 +82,10 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d) {
case SCR_DYN_OBJ: {
const ScrDyn *marker = scr_dyn_obj_get(d, "%error", 6);
if (marker) {
const ScrDyn *en = scr_dyn_obj_get(d, "name", 4);
const ScrDyn *em = scr_dyn_obj_get(d, "message", 7);
const ScrStr *ens = (en && en->kind == SCR_DYN_STR) ? en->v.str : NULL;
const ScrStr *ems = (em && em->kind == SCR_DYN_STR) ? em->v.str : NULL;
if (ens) scr_jb_write(b, ens->data, ens->len);
if (ens && ens->len && ems && ems->len) scr_jb_puts(b, ": ");
if (ems) scr_jb_write(b, ems->data, ems->len);
ScrStr *text = scr_dyn_error_string(d);
if (!text) return;
scr_jb_write(b, text->data, text->len);
scr_str_release(text);
return;
}
scr_jb_puts(b, "[object Object]");
+15
View File
@@ -1937,6 +1937,21 @@ ScrPromise *scr_tp_set_timeout_signal(double ms, ScrPromise *value, bool ref, Sc
return scr_tp_signal(ms, value, ref, signal, false);
}
ScrPromise *scr_tp_scheduler_wait_signal(double ms, ScrPromise *value, bool ref, ScrDyn *signal) {
if (scr_mt_wait_state == 2) {
static const char msg[] = "Value of \"this\" must be of type Scheduler";
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_THIS");
return NULL;
}
if (scr_mt_wait_state == 1) {
static const char msg[] =
"scheduler.wait with a signal while mock.timers mocks it has no lowering";
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020");
return NULL;
}
return scr_tp_signal(ms, value, ref, signal, false);
}
ScrPromise *scr_tp_set_immediate_signal(ScrPromise *value, bool ref, ScrDyn *signal) {
return scr_tp_signal(0, value, ref, signal, true);
}
+5
View File
@@ -108,6 +108,11 @@ ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener);
* `value` is the already-resolved promise of the resolve value, borrowed.
* Never throw; +1. */
ScrPromise *scr_tp_set_timeout_signal(double ms, ScrPromise *value, bool ref, ScrDyn *signal);
/* scheduler.wait(delay, { signal }) in a program that uses mock.timers:
* scr_tp_set_timeout_signal, except that it throws Node's ERR_INVALID_THIS
* once a reset restored the real scheduler.wait, and SC2020 while
* mock.timers mocks it. NULL after a throw. */
ScrPromise *scr_tp_scheduler_wait_signal(double ms, ScrPromise *value, bool ref, ScrDyn *signal);
ScrPromise *scr_tp_set_immediate_signal(ScrPromise *value, bool ref, ScrDyn *signal);
/* Compiled subclasses (`class Bus extends EventTarget`, `class Ping
@@ -1115,3 +1115,11 @@ void scr_emitter_set_default_max(double n) {
scr_emitter_default_max = n;
}
double scr_emitter_get_default_max(void) { return scr_emitter_default_max; }
/* The events module's ES module namespace holds a copy of
* defaultMaxListeners, taken when the program starts; ESM namespace
* and named-import reads answer it. module.syncBuiltinESMExports() copies
* the current value into it. */
static SCR_TL double scr_emitter_esm_default_max = 10;
double scr_emitter_esm_default_max_get(void) { return scr_emitter_esm_default_max; }
void scr_emitter_esm_sync(void) { scr_emitter_esm_default_max = scr_emitter_default_max; }
@@ -143,6 +143,8 @@ ScrEmitter *scr_emitter_set_max(ScrEmitter *em, double n); /* returns em +1 */
double scr_emitter_get_max(ScrEmitter *em);
void scr_emitter_set_default_max(double n);
double scr_emitter_get_default_max(void);
double scr_emitter_esm_default_max_get(void);
void scr_emitter_esm_sync(void);
/* True when the emitter has at least one listener for `name` — the
* stream unit's registration probe (and anyone else's). */
bool scr_emitter_has(ScrEmitter *em, const char *name);
+18 -3
View File
@@ -496,6 +496,15 @@ static void sf_signal_timer_fire(ScrClosure *cb) {
sf_signal_release(s);
}
/* The mocked AbortSignal.timeout's timer: Node's mock calls abort() on
* the signal's controller, so the reason is the AbortError. */
static void sf_signal_mock_timer_fire(ScrClosure *cb) {
SfSignal *s = (SfSignal *)scr_box_get_ref(cb->caps[0]);
if (!s) return;
if (!s->aborted) sf_signal_abort_default(s, false);
sf_signal_release(s);
}
ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay) {
if (!delay || delay->kind != SCR_DYN_NUM) {
if (delay) {
@@ -527,13 +536,19 @@ ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay) {
return NULL;
}
SfSignal *s = sf_signal_new();
ScrClosure *cb = scr_closure_new((void *)&sf_signal_timer_fire, 1);
bool mocked = scr_mt_abort_timeout_arm != NULL;
ScrClosure *cb = scr_closure_new(
mocked ? (void *)&sf_signal_mock_timer_fire : (void *)&sf_signal_timer_fire, 1);
ScrBox *box =
scr_box_new_obj(&sf_signal_retain_v, &sf_signal_release_v, NULL);
scr_box_set_ref(box, sf_signal_retain(s));
cb->caps[0] = box;
s->timer_id = scr_set_timeout_handle(cb, ms);
scr_timer_unref(s->timer_id);
if (mocked) {
scr_mt_abort_timeout_arm(cb, ms);
} else {
s->timer_id = scr_set_timeout_handle(cb, ms);
scr_timer_unref(s->timer_id);
}
ScrDyn *out = scr_dyn_new_handle(s, SCR_DYNH_ABORT_SIGNAL);
sf_signal_release(s);
return out;
+1
View File
@@ -1243,6 +1243,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
if (d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET)
return scr_insp_weak_collection(d->v.handle.tag == SCR_DYNH_WEAK_MAP ? "WeakMap" : "WeakSet",
recurse, depth);
if (d->v.handle.tag == SCR_DYNH_DATE) return scr_insp_date(scr_dyn_native_date_value(d));
const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(d);
if (ops->inspect_view) {
/* Node's custom inspectors answer the bare class name beyond the
+3
View File
@@ -428,11 +428,13 @@ static const ScrITableEntry *scr_itable_insert_bytes(ScrITable *t, uint64_t hash
const ScrITableEntry *scr_itable_insert_str(ScrITable *t, const ScrStr *key, uint64_t value) {
scr_itable_enter();
scr_sa_thread_shares(); /* the stored value graph is read by every thread */
return scr_itable_insert_bytes(t, scr_itable_str_hash(key), key->data, key->len, value);
}
const ScrITableEntry *scr_itable_insert_f64(ScrITable *t, double key, uint64_t value) {
scr_itable_enter();
scr_sa_thread_shares(); /* the stored value graph is read by every thread */
uint64_t stored = scr_itable_f64_bits(key) ^ SCR_ITABLE_F64_FLIP;
uint64_t hash = scr_itable_f64_hash(stored);
ScrITableSegment *seg = scr_itable_segment(t, hash);
@@ -588,6 +590,7 @@ const ScrITableEntry *scr_itable_find_arr(ScrITable *t, const ScrArr *key) {
const ScrITableEntry *scr_itable_insert_arr(ScrITable *t, const ScrArr *key, uint64_t value) {
scr_itable_enter();
scr_sa_thread_shares(); /* the stored value graph is read by every thread */
char stack[SCR_ITABLE_STACK_KEY];
size_t len;
char *bytes = scr_itable_encoded(t, key, stack, sizeof stack, &len);
+8 -2
View File
@@ -8,13 +8,18 @@
#include <stdlib.h>
#include <string.h>
/* The table unit needs only scr_trap and scr_throw_error_msg from the
* rest of the runtime; refusals are recorded here. */
/* The table unit needs only scr_trap, scr_throw_error_msg and
* scr_sa_thread_shares from the rest of the runtime; refusals and the
* inserting threads' shares are recorded here. */
_Noreturn void scr_trap(const char *msg) {
fputs(msg, stderr);
abort();
}
/* Inserts share the stored values with every thread. */
static atomic_int shares;
void scr_sa_thread_shares(void) { atomic_fetch_add(&shares, 1); }
static char refused[128];
void scr_throw_error_msg(int kind, const char *msg, size_t len) {
assert(kind == SCR_ERR_TYPE && len < sizeof refused);
@@ -222,6 +227,7 @@ int main(void) {
for (unsigned i = 1; i < THREADS; i++) assert(seen_str[i][k] == seen_str[0][k]);
assert(scr_itable_size(strings) == KEYS);
assert(scr_itable_size(numbers) == KEYS);
assert(atomic_load(&shares) >= 2 * KEYS);
printf("intern tables share one entry per key across racing inserts and lookups\n");
return 0;
}
+459 -125
View File
@@ -108,6 +108,26 @@ static void scr_async_iterator_symbol_cleanup(void) {
scr_async_iterator_symbol = NULL;
}
static SCR_TL ScrDyn *scr_async_dispose_symbol;
static void scr_async_dispose_symbol_cleanup(void) {
scr_dyn_release(scr_async_dispose_symbol);
scr_async_dispose_symbol = NULL;
}
/* Symbol.asyncDispose, registered by the symbol unit when the program
* first names it, for handle classes outside that unit (a Worker). */
void scr_dyn_install_async_dispose_symbol(ScrDyn *key) {
if (scr_async_dispose_symbol) return;
scr_async_dispose_symbol = scr_dyn_retain(key);
scr_atexit(scr_async_dispose_symbol_cleanup);
}
bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key) {
return scr_async_dispose_symbol && key->kind == SCR_DYN_SYMBOL &&
key->v.symbol.value == scr_async_dispose_symbol->v.symbol.value;
}
static void scr_to_primitive_symbol_cleanup(void) {
scr_dyn_release(scr_to_primitive_symbol);
scr_to_primitive_symbol = NULL;
@@ -482,6 +502,7 @@ typedef struct ScrJsonSeenEnt {
const void *ptr;
bool is_array;
const char *prop; /* static property edge (emitted C literal) */
size_t prop_len; /* its length, or SIZE_MAX for a NUL-terminated name */
const ScrStr *prop_str; /* overflow-key edge (borrowed for the write) */
size_t index; /* array/tuple index edge */
} ScrJsonSeenEnt;
@@ -489,7 +510,7 @@ typedef struct ScrJsonSeenEnt {
static void scr_jb_put_edge(ScrJsonBuf *m, const ScrJsonSeenEnt *e) {
if (e->prop || e->prop_str) {
scr_jb_puts(m, "property '");
if (e->prop) scr_jb_puts(m, e->prop);
if (e->prop) scr_jb_write(m, e->prop, e->prop_len == SIZE_MAX ? strlen(e->prop) : e->prop_len);
else scr_jb_write(m, e->prop_str->data, e->prop_str->len);
scr_jb_putc(m, '\'');
return;
@@ -539,6 +560,7 @@ bool scr_jb_enter(ScrJsonBuf *b, const void *v, bool is_array) {
e->ptr = v;
e->is_array = is_array;
e->prop = NULL;
e->prop_len = SIZE_MAX;
e->prop_str = NULL;
e->index = 0;
return true;
@@ -551,6 +573,16 @@ void scr_jb_leave(ScrJsonBuf *b) {
void scr_jb_edge_prop(ScrJsonBuf *b, const char *name) {
ScrJsonSeenEnt *e = &b->seen[b->seen_len - 1];
e->prop = name;
e->prop_len = SIZE_MAX;
e->prop_str = NULL;
}
/* A property edge whose name is not NUL-terminated (a declared field name
* from a record source's strided table). */
static void scr_jb_edge_span(ScrJsonBuf *b, const char *name, size_t len) {
ScrJsonSeenEnt *e = &b->seen[b->seen_len - 1];
e->prop = name;
e->prop_len = len;
e->prop_str = NULL;
}
@@ -2646,54 +2678,116 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec) {
return merged;
}
static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key);
static bool scr_jb_try_pure_member(ScrJsonBuf *b, const char *key, size_t key_len,
const ScrStr *key_str, const ScrDyn *value, bool *first);
static bool scr_jb_put_member(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value, bool *first,
void *rec);
/* JSON for a record that kept a source: the view's own-key order, declared
* fields through the compiled writer (`field` writes the separator, key
* and value of field `index` unless the record lacks it, and answers
* whether it wrote), every other enumerable property through the dyn
* writer with JSON.stringify's member rules, then declared fields the view
* had not seen. May leave an exception pending (a BigInt property). */
* whether it wrote), every other enumerable property with JSON.stringify's
* member rules (accessors read, toJSON called with the key, undefined,
* function and symbol results dropped, circular structures rejected), then
* declared fields the view had not seen. May leave an exception pending
* (a BigInt property, a throwing toJSON or getter, a circular structure). */
void scr_jb_put_record_source(ScrJsonBuf *b, void *rec,
bool (*field)(ScrJsonBuf *, void *, size_t, bool)) {
if (!scr_jb_try_record_source(b, rec, field)) scr_jb_puts(b, "{}");
}
/* Writes declared field `index`; when the record is on the circular-
* structure stack, the field's name becomes its edge first. */
static bool scr_jb_put_source_field(ScrJsonBuf *b, void *rec, const ScrRecordSource *source,
size_t index, bool first, bool on_stack,
bool (*field)(ScrJsonBuf *, void *, size_t, bool)) {
if (on_stack) {
size_t len;
memcpy(&len, source->lens + index * source->len_stride, sizeof len);
const char *name;
memcpy(&name, source->names + index * source->name_stride, sizeof name);
scr_jb_edge_span(b, name, len);
}
return field(b, rec, index, first);
}
/* The same in one lookup: writes the record and answers true when it kept
* a source, otherwise writes nothing and answers false. */
bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec,
bool (*field)(ScrJsonBuf *, void *, size_t, bool)) {
ScrRecordSource *source = scr_record_source_find(rec);
if (!source) return false;
ScrRecordSource *found = scr_record_source_find(rec);
if (!found) return false;
/* Program code run by a member can add record sources and move the
* table's entries, so the walk keeps its own copy. */
const ScrRecordSource source = *found;
/* Walkers over cycle-capable shapes have pushed the record already.
* Others push it before the first member that needs the protocol walk,
* so a member leading back to the record closes the circle at the
* record. */
bool on_stack = b->seen_len && b->seen[b->seen_len - 1].ptr == rec;
bool entered = false;
ScrDyn *view = scr_dyn_retain(source.view);
scr_jb_putc(b, '{');
bool first = true;
{
size_t count = source->count;
bool small[64] = { false };
bool *seen = count <= 64 ? small : calloc(count, sizeof *seen);
if (!seen) scr_json_oom();
const ScrDyn *view = source->view;
for (size_t i = 0; i < view->v.obj.len && !scr_exc_pending(); i++) {
const ScrDynEntry *entry = &view->v.obj.entries[i];
size_t index = scr_record_source_index(source, entry->key, entry->key_len);
size_t count = source.count;
bool small[64] = { false };
bool *seen = count <= 64 ? small : calloc(count, sizeof *seen);
if (!seen) scr_json_oom();
/* Members whose writing cannot run program code are written in place.
* The first member that can (an accessor, a toJSON method, a getter below
* it) ends that loop: the remaining keys are taken first, as
* SerializeJSONObject takes them before reading any value. */
size_t i = 0;
for (; i < view->v.obj.len && !scr_exc_pending(); i++) {
const ScrDynEntry *entry = &view->v.obj.entries[i];
size_t index = scr_record_source_index(&source, entry->key, entry->key_len);
if (index < count) {
if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field))
first = false;
seen[index] = true;
continue;
}
if (!entry->enumerable) continue;
if (entry->accessor ||
!scr_jb_try_pure_member(b, entry->key, entry->key_len, NULL, entry->value, &first))
break;
}
if (i < view->v.obj.len && !scr_exc_pending() && !on_stack) {
entered = scr_jb_enter(b, rec, false);
on_stack = entered;
}
if (i < view->v.obj.len && !scr_exc_pending()) {
ScrDyn *rest = scr_dyn_new_arr();
for (size_t j = i; j < view->v.obj.len; j++) {
const ScrDynEntry *entry = &view->v.obj.entries[j];
if (!entry->enumerable && !scr_record_source_declared(&source, entry->key, entry->key_len)) continue;
ScrStr *name = scr_str_new(entry->key, entry->key_len);
scr_dyn_arr_push(rest, scr_dyn_new_str(name));
scr_str_release(name);
}
for (size_t j = 0; j < rest->v.arr.len && !scr_exc_pending(); j++) {
const ScrStr *key = rest->v.arr.items[j]->v.str;
size_t index = scr_record_source_index(&source, key->data, key->len);
if (index < count) {
if (!seen[index] && field(b, rec, index, first)) first = false;
if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field))
first = false;
seen[index] = true;
continue;
}
if (!entry->enumerable || entry->accessor) continue;
ScrDynKind kind = entry->value->kind;
if (kind == SCR_DYN_UNDEF || kind == SCR_DYN_FUNC || kind == SCR_DYN_SYMBOL) continue;
if (!first) scr_jb_putc(b, ',');
first = false;
scr_jb_put_json_span(b, entry->key, entry->key_len);
scr_jb_putc(b, ':');
scr_jb_put_dyn(b, entry->value);
ScrDyn *value = scr_json_member(view, key);
if (!value || !scr_jb_put_member(b, key, value, &first, NULL)) break;
}
for (size_t index = 0; index < count && !scr_exc_pending(); index++) {
if (!seen[index] && field(b, rec, index, first)) first = false;
}
if (seen != small) free(seen);
scr_dyn_release(rest);
}
for (size_t index = 0; index < count && !scr_exc_pending(); index++) {
if (!seen[index] && scr_jb_put_source_field(b, rec, &source, index, first, on_stack, field))
first = false;
}
if (seen != small) free(seen);
scr_dyn_release(view);
scr_jb_putc(b, '}');
if (entered) scr_jb_leave(b);
return true;
}
@@ -5947,11 +6041,28 @@ void scr_errdyn_put(ScrError *e, ScrDyn *d) { scr_errdyn_append(e, d); }
static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
bool cause = key->len == 5 && memcmp(key->data, "cause", 5) == 0;
bool stack = key->len == 5 && memcmp(key->data, "stack", 5) == 0;
if (!cause && !stack) return;
bool name = key->len == 4 && memcmp(key->data, "name", 4) == 0;
bool message = key->len == 7 && memcmp(key->data, "message", 7) == 0;
if (!cause && !stack && !name && !message) return;
size_t hit = scr_errdyn_find(view, false);
if (hit == SIZE_MAX) return;
ScrDynEntry *entry = scr_dyn_entry(view, key);
ScrError *e = scr_errdyn_cache[hit].err;
if (name || message) {
/* The native error's toString and inspect read these fields. */
if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return;
ScrStr **field = name ? &e->name : &e->message;
scr_str_release(*field);
*field = scr_str_retain(entry->value->v.str);
if (name) {
e->name_present = true;
e->name_enumerable = entry->enumerable;
} else {
e->message_present = true;
e->message_enumerable = entry->enumerable;
}
return;
}
if (stack) {
if (entry && entry->value->kind == SCR_DYN_STR) {
scr_str_release(e->stack);
@@ -5967,6 +6078,46 @@ static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) {
e->cause_enumerable = entry && entry->enumerable;
}
/* Error.prototype.toString over the checked-dynamic error encoding (an
* object carrying the reserved "%error" marker): `name` defaults to "Error"
* and `message` to "", both read through the prototype; either empty leaves
* the other alone, otherwise "name: message". NULL with a pending exception
* when a member's string conversion throws. */
ScrStr *scr_dyn_error_string(const ScrDyn *d) {
ScrStr *parts[2] = { NULL, NULL };
static const char *const keys[2] = { "name", "message" };
for (int i = 0; i < 2; i++) {
ScrDyn *member = scr_dyn_obj_read(d, keys[i], strlen(keys[i]));
if (!member || member->kind == SCR_DYN_UNDEF) {
parts[i] = i == 0 ? scr_str_new("Error", 5) : scr_str_new("", 0);
} else {
parts[i] = scr_dyn_string_coerce_js(member);
}
scr_dyn_release(member);
if (!parts[i]) {
scr_str_release(parts[0]);
return NULL;
}
}
if (parts[0]->len == 0 || parts[1]->len == 0) {
ScrStr *out = parts[0]->len == 0 ? parts[1] : parts[0];
scr_str_release(out == parts[0] ? parts[1] : parts[0]);
return out;
}
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_put_str(&b, parts[0]);
scr_jb_puts(&b, ": ");
scr_jb_put_str(&b, parts[1]);
scr_str_release(parts[0]);
scr_str_release(parts[1]);
return scr_jb_finish(&b);
}
static bool scr_dyn_error_encoding(const ScrDyn *d) {
return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL;
}
/* Receiver-kind-dispatched toString() on a checked-dynamic value (the
* dyn method surface — a stream's 'data'/for-await chunk is the common
* receiver): bytes decode per the encoding (Node's Buffer.toString,
@@ -6002,6 +6153,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
case SCR_DYN_BOOL:
return d->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5);
case SCR_DYN_OBJ:
if (scr_dyn_error_encoding(d) && !scr_dyn_property_owner(d, "toString", 8)) return scr_dyn_error_string(d);
return scr_str_new("[object Object]", 15);
case SCR_DYN_HANDLE:
if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL) {
@@ -6396,6 +6548,13 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) {
* number-hint protocol advances to toString. */
continue;
}
if (i == 1 && scr_dyn_error_encoding(d)) {
ScrStr *text = scr_dyn_error_string(d);
if (!text) return false;
*out = scr_string_to_number(text);
scr_str_release(text);
return true;
}
if (scr_dyn_has_object_prototype(d) && i == 1) {
*out = NAN; /* Number("[object Object]") */
return true;
@@ -6511,6 +6670,13 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) {
const ScrDyn *owner = scr_dyn_property_owner(d, methods[i], strlen(methods[i]));
ScrDyn *method = owner ? scr_dyn_obj_read(d, methods[i], strlen(methods[i])) : NULL;
if (!method) {
if (i == 1 && scr_dyn_error_encoding(d)) {
ScrStr *str = scr_dyn_error_string(d);
if (!str) return NULL;
ScrDyn *out = scr_dyn_new_str(str);
scr_str_release(str);
return out;
}
if (i == 1 && scr_dyn_has_object_prototype(d)) {
ScrStr *str = scr_str_new("[object Object]", 15);
ScrDyn *out = scr_dyn_new_str(str);
@@ -7064,99 +7230,6 @@ void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value) {
scr_str_release(name);
}
/* Node's JSON.stringify over a dyn: object members holding undefined DROP,
* array slots holding undefined print null. A bare undefined never arrives
* (the record serializer drops the entry first); print null defensively. */
void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("JSON.stringify"); return; }
switch (d->kind) {
case SCR_DYN_NULL:
case SCR_DYN_UNDEF:
case SCR_DYN_SYMBOL:
case SCR_DYN_FUNC: /* JSON.stringify: functions serialize like undefined */
scr_jb_puts(b, "null");
return;
case SCR_DYN_BOOL:
scr_jb_puts(b, d->v.b ? "true" : "false");
return;
case SCR_DYN_BIGINT: {
static const char message[] = "Do not know how to serialize a BigInt";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return;
}
case SCR_DYN_NUM:
scr_jb_put_f64(b, d->v.num);
return;
case SCR_DYN_STR:
scr_jb_put_json_str(b, d->v.str);
return;
case SCR_DYN_BYTES: {
/* Node's JSON.stringify over a typed array: the index-keyed object
* form — {"0":1,"1":2}, using each view's numeric element kind. */
scr_jb_putc(b, '{');
for (size_t i = 0; i < d->v.bytes->len; i++) {
if (i > 0) scr_jb_putc(b, ',');
char idx[32];
snprintf(idx, sizeof idx, "\"%zu\":", i);
scr_jb_puts(b, idx);
scr_jb_put_f64(b, scr_bytes_get(d->v.bytes, (double)i));
}
scr_jb_putc(b, '}');
return;
}
case SCR_DYN_ARR:
scr_jb_putc(b, '[');
for (size_t i = 0; i < d->v.arr.len; i++) {
if (i > 0) scr_jb_putc(b, ',');
scr_jb_put_dyn(b, d->v.arr.items[i]);
}
scr_jb_putc(b, ']');
return;
case SCR_DYN_PROMISE:
/* No own enumerable properties — Node stringifies a promise as {}. */
scr_jb_puts(b, "{}");
return;
case SCR_DYN_HANDLE: {
/* Node's JSON.stringify over these classes throws the circular-
* structure TypeError with a V8 path dump we cannot reproduce —
* throw "not supported yet" instead of a wrong shape (SEMANTICS.md). */
ScrJsonBuf m;
scr_jb_init(&m);
scr_jb_puts(&m, "JSON.stringify of a dynamic ");
scr_jb_puts(&m, scr_dyn_handle_cls(d));
scr_jb_puts(&m, " is not supported yet");
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&m));
scr_jb_puts(b, "null"); /* the buffer never surfaces: the pending throw wins */
return;
}
case SCR_DYN_TYPED_REF: {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
scr_jb_put_dyn(b, materialized);
scr_dyn_release(materialized);
return;
}
case SCR_DYN_OBJ: {
scr_jb_putc(b, '{');
bool first = true;
for (size_t i = 0; i < d->v.obj.len; i++) {
const ScrDynEntry *e = &d->v.obj.entries[i];
if (!e->enumerable) continue;
if (e->value->kind == SCR_DYN_UNDEF || e->value->kind == SCR_DYN_FUNC || e->value->kind == SCR_DYN_SYMBOL) continue; /* dropped, like Node */
/* engine functions drop too */
if (!first) scr_jb_putc(b, ',');
first = false;
scr_jb_put_json_span(b, e->key, e->key_len);
scr_jb_putc(b, ':');
scr_jb_put_dyn(b, e->value);
}
scr_jb_putc(b, '}');
return;
}
}
scr_trap("scriptc: internal error: invalid dyn kind\n");
}
/* ── dynCheck failure path ─────────────────────────────────────────────── */
static void scr_dyn_path_render(ScrJsonBuf *b, const ScrDynPath *p) {
@@ -8941,9 +9014,12 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) {
return true;
}
static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const ScrDyn *replacer) {
ScrDyn *value = scr_json_member(holder, key);
if (!value) return NULL;
/* SerializeJSONProperty's toJSON step over a member value (consumed): Date,
* URL and net handles convert through their toJSON, a Buffer through its
* { type, data } view, and an object with a callable toJSON through that
* method, called with the key. Returns the value to serialize (+1), or NULL
* with an exception pending. */
static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) {
/* Inspect a typed reference without replacing its live identity. Later
* members must read the source again after a callback mutates it. A live
* array, or a live record without a `toJSON` field, needs no conversion. */
@@ -8978,6 +9054,13 @@ static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const S
} else scr_dyn_release(method);
}
scr_dyn_release(view);
return value;
}
static ScrDyn *scr_json_replace(const ScrDyn *holder, const ScrStr *key, const ScrDyn *replacer) {
ScrDyn *value = scr_json_member(holder, key);
if (!value) return NULL;
value = scr_json_to_json(value, key);
if (!value) return NULL;
ScrDyn *result = scr_json_callback(replacer, holder, key, value);
scr_dyn_release(value);
@@ -9273,6 +9356,197 @@ ScrStr *scr_util_format_j_text(ScrStr *text) {
return scr_json_circular_or_rethrow();
}
/* The gap of a compact walk; the walks read only its length. */
static const ScrStr scr_json_compact_gap = { .rc = SIZE_MAX, .len = 0 };
#define SCR_JSON_PURE_DEPTH 32
/* Writes `value` when writing it runs no program code and meets no cycle:
* JSON primitives, and objects and dense arrays with the default prototype
* whose members are such values, where objects hold only data properties
* under names that are neither array indexes nor "toJSON" (so their own
* key order is their insertion order and no toJSON step applies). Answers
* false, having written part of the value, for anything else, including
* nesting deeper than SCR_JSON_PURE_DEPTH; the caller then truncates the
* buffer and runs the protocol walk. */
static bool scr_jb_put_pure(ScrJsonBuf *b, const ScrDyn *value, size_t depth) {
switch (value->kind) {
case SCR_DYN_NULL: scr_jb_puts(b, "null"); return true;
case SCR_DYN_BOOL: scr_jb_puts(b, value->v.b ? "true" : "false"); return true;
case SCR_DYN_NUM: scr_jb_put_f64(b, value->v.num); return true;
case SCR_DYN_STR: scr_jb_put_json_str(b, value->v.str); return true;
case SCR_DYN_ARR:
if (value->prototype || value->v.arr.presence || value->v.arr.properties ||
depth == SCR_JSON_PURE_DEPTH)
return false;
scr_jb_putc(b, '[');
for (size_t i = 0; i < value->v.arr.len; i++) {
if (i > 0) scr_jb_putc(b, ',');
const ScrDyn *item = value->v.arr.items[i];
if (scr_json_omitted(item)) scr_jb_puts(b, "null");
else if (!scr_jb_put_pure(b, item, depth + 1)) return false;
}
scr_jb_putc(b, ']');
return true;
case SCR_DYN_OBJ: {
if (value->prototype || depth == SCR_JSON_PURE_DEPTH) return false;
scr_jb_putc(b, '{');
bool first = true;
for (size_t i = 0; i < value->v.obj.len; i++) {
const ScrDynEntry *entry = &value->v.obj.entries[i];
if (!entry->enumerable) continue;
double index;
if (entry->accessor || (entry->key_len == 6 && memcmp(entry->key, "toJSON", 6) == 0) ||
(entry->key_len && (unsigned char)(entry->key[0] - '0') < 10 &&
scr_dyn_key_is_index(entry->key, entry->key_len, &index)))
return false;
if (scr_json_omitted(entry->value)) continue;
if (!first) scr_jb_putc(b, ',');
first = false;
scr_jb_put_json_span(b, entry->key, entry->key_len);
scr_jb_putc(b, ':');
if (!scr_jb_put_pure(b, entry->value, depth + 1)) return false;
}
scr_jb_putc(b, '}');
return true;
}
default:
return false;
}
}
/* Writes the separator, key and value of an object member whose value
* scr_jb_put_pure can write, or drops a member holding undefined, a
* function or a symbol. `key_str`, when not NULL, is the key as a string.
* Answers false, with the buffer and `first` as they were, for a value
* that needs the protocol walk. */
static inline __attribute__((always_inline)) bool scr_jb_try_pure_member(
ScrJsonBuf *b, const char *key, size_t key_len, const ScrStr *key_str, const ScrDyn *value,
bool *first) {
if (scr_json_omitted(value)) return true;
size_t mark = b->len;
if (!*first) scr_jb_putc(b, ',');
if (key_str) scr_jb_put_json_str(b, key_str);
else scr_jb_put_json_span(b, key, key_len);
scr_jb_putc(b, ':');
if (scr_jb_put_pure(b, value, 0)) {
*first = false;
return true;
}
b->len = mark;
return false;
}
/* Writes one object member of a typed container: `value` (consumed) goes
* through its toJSON step, an undefined, function or symbol result drops
* the member, and any other result is written after the separator and the
* key through the protocol walk, whose circular-structure checks share the
* buffer's stack. The container is the top of that stack, or `rec` is
* pushed for the walk when it is not; the key becomes the container's
* edge. Answers false with an exception pending. */
static bool scr_jb_put_member_walk(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value,
bool *first, void *rec) {
ScrDyn *child = scr_json_to_json(value, key);
if (!child) return false;
if (scr_json_omitted(child)) {
scr_dyn_release(child);
return true;
}
bool entered = rec && !(b->seen_len && b->seen[b->seen_len - 1].ptr == rec);
if (entered && !scr_jb_enter(b, rec, false)) {
scr_dyn_release(child);
return false;
}
if (!*first) scr_jb_putc(b, ',');
*first = false;
scr_jb_put_json_str(b, key);
scr_jb_putc(b, ':');
scr_jb_edge_key(b, key);
bool ok = scr_json_replaced_write(b, child, scr_dyn_undefined(), NULL, &scr_json_compact_gap,
b->seen_len);
if (entered) scr_jb_leave(b);
scr_dyn_release(child);
return ok;
}
/* scr_jb_put_member_walk, writing the member directly when scr_jb_put_pure
* can write its value. */
static bool scr_jb_put_member(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value, bool *first,
void *rec) {
if (scr_jb_try_pure_member(b, key->data, key->len, key, value, first)) {
scr_dyn_release(value);
return true;
}
return scr_jb_put_member_walk(b, key, value, first, rec);
}
/* An entry of a record's `unknown` index signature, written after the
* record's earlier members with JSON.stringify's member rules
* (scr_jb_put_member). Walkers over cycle-capable shapes have pushed the
* record already; others push it for an entry that needs the protocol
* walk. May leave an exception pending. */
static void scr_jb_put_dyn_entry(ScrJsonBuf *b, void *rec, const ScrStr *key,
const ScrDyn *value, bool *first) {
if (scr_jb_try_pure_member(b, key->data, key->len, key, value, first)) return;
scr_jb_put_member_walk(b, key, scr_dyn_retain((ScrDyn *)value), first, rec);
}
/* The entries of record `rec`'s `unknown` index signature (`map`: string
* keys, checked-dynamic values) in JS own-key order, each with
* scr_jb_put_dyn_entry's rules. Without array-index keys that order is the
* map's insertion order, walked in place: the walk holds the map's
* iteration count so no compaction moves entries, rereads each entry by
* position, and stops at the entries present when it started, so a toJSON
* call that adds or deletes entries sees Node's result (keys taken first,
* deleted ones read as undefined). With array-index keys the walk takes the
* ordered key list first. May leave an exception pending. */
void scr_jb_put_dyn_entries(ScrJsonBuf *b, void *rec, ScrMap *map, bool *first) {
uint32_t count = map->nentries;
bool indexed = false;
for (uint32_t i = 0; i < count && !indexed; i++) {
if (!map->entries[i].hash) continue;
const ScrStr *key = (const ScrStr *)(uintptr_t)map->entries[i].key;
uint32_t index;
indexed = scr_key_array_index(key->data, key->len, &index);
}
if (indexed) {
ScrArr *keys = scr_map_keys_js_order(map);
for (size_t i = 0; i < keys->len && !scr_exc_pending(); i++) {
ScrStr *key = scr_arr_get_ref(keys, (double)i);
ScrDyn *value = scr_map_get_str_ref(map, key);
if (value) {
scr_jb_put_dyn_entry(b, rec, key, value, first);
scr_dyn_release(value);
}
scr_str_release(key);
}
scr_arr_release(keys);
return;
}
map->iter_depth++;
for (uint32_t i = 0; i < count && !scr_exc_pending(); i++) {
const ScrMapEntry *entry = &map->entries[i];
if (!entry->hash) continue;
const ScrStr *key = (const ScrStr *)(uintptr_t)entry->key;
const ScrDyn *value = (const ScrDyn *)(uintptr_t)entry->val;
if (scr_jb_try_pure_member(b, key->data, key->len, key, value, first)) continue;
/* The walk can run program code, which may replace this entry. */
ScrStr *held_key = scr_str_retain((ScrStr *)key);
scr_jb_put_member_walk(b, held_key, scr_dyn_retain((ScrDyn *)value), first, rec);
scr_str_release(held_key);
}
map->iter_depth--;
}
/* Writes a dyn value whose toJSON step has run through the protocol walk:
* undefined, functions and symbols as null (an array slot's rule), and
* containers with toJSON, accessor and circular-structure handling. May
* leave an exception pending. */
void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d) {
if (scr_json_omitted(d)) scr_jb_puts(b, "null");
else scr_json_replaced_write(b, d, scr_dyn_undefined(), NULL, &scr_json_compact_gap, b->seen_len);
}
/* Untyped RC adapters (box/promise/exception-cell currency). */
void *scr_dyn_retain_v(void *d) { return scr_dyn_retain((ScrDyn *)d); }
void scr_dyn_release_v(void *d) { scr_dyn_release((ScrDyn *)d); }
@@ -9489,6 +9763,61 @@ void scr_sc_validate_options(const ScrDyn *options) {
scr_dyn_release(transferView);
}
/* ── markAsUncloneable ─────────────────────────────────────────────── */
/* The plain objects, errors and class instances marked with
* markAsUncloneable (node:worker_threads): structuredClone and
* postMessage throw DataCloneError for them. Node's mark has no effect on
* primitives, arrays, Maps, Sets, Dates, typed arrays and ArrayBuffers.
* The marked values are retained until the thread exits, so an identity
* is never reused while marked. */
static SCR_TL ScrDyn **scr_uncloneable;
SCR_TL size_t scr_uncloneable_count = 0;
static SCR_TL size_t scr_uncloneable_capacity;
static void scr_uncloneable_cleanup(void) {
for (size_t i = 0; i < scr_uncloneable_count; i++) scr_dyn_release(scr_uncloneable[i]);
free(scr_uncloneable);
scr_uncloneable = NULL;
scr_uncloneable_count = scr_uncloneable_capacity = 0;
}
/* The identity a mark is kept under: a class instance's or a record's
* object, or a plain object's source (its own box otherwise); NULL for
* values Node does not mark. */
static const void *scr_uncloneable_identity(const ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF &&
((value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) ||
(value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "record:", 7))))
return value->v.typed_ref.ptr;
if (value->kind == SCR_DYN_OBJ)
return value->v.obj.source_identity ? value->v.obj.source_identity : (const void *)value;
return NULL;
}
void scr_mark_uncloneable(ScrDyn *value) {
const void *identity = scr_uncloneable_identity(value);
if (!identity || scr_uncloneable_marked(value)) return;
if (scr_uncloneable_count == scr_uncloneable_capacity) {
if (!scr_uncloneable_capacity) scr_atexit(scr_uncloneable_cleanup);
size_t capacity = scr_uncloneable_capacity ? scr_uncloneable_capacity * 2 : 8;
ScrDyn **values = realloc(scr_uncloneable, capacity * sizeof *values);
if (!values) scr_trap("scriptc: out of memory\n");
scr_uncloneable = values;
scr_uncloneable_capacity = capacity;
}
scr_uncloneable[scr_uncloneable_count++] = scr_dyn_retain(value);
}
bool scr_uncloneable_marked(const ScrDyn *value) {
if (scr_uncloneable_count == 0) return false;
const void *identity = scr_uncloneable_identity(value);
if (!identity) return false;
for (size_t i = 0; i < scr_uncloneable_count; i++)
if (scr_uncloneable_identity(scr_uncloneable[i]) == identity) return true;
return false;
}
/* The parent chain rides the C stack: a revisit is a cycle. */
typedef struct ScrScParent {
const ScrDyn *node;
@@ -9499,6 +9828,10 @@ typedef struct ScrScParent {
* throw), so it is tagged like a parse result (ScrDynRegion); a leaf the
* region may not hold clears it. */
static ScrDyn *scr_sc_clone(const ScrDyn *v, const ScrScParent *up, ScrDynRegion *region) {
if (scr_uncloneable_count && scr_uncloneable_marked(v)) {
scr_throw_domex("DataCloneError", "Cannot clone object of unsupported type.");
return NULL;
}
if (v->kind == SCR_DYN_PROXY) {
scr_throw_domex("DataCloneError", "#<Object> could not be cloned.");
return NULL;
@@ -10558,6 +10891,7 @@ static ScrArr *scr_record_shared_keys(const ScrDynTypedRecordOps *record) {
#ifdef SCR_RC_AUDIT
scr_arr_live_forget(); /* immortal: outside the exit-time RC audit */
#endif
scr_sa_thread_shares(); /* every thread reads the array */
return built;
}
built->rc = 1;
+9 -2
View File
@@ -162,9 +162,16 @@ void scr_jb_put_number(ScrJsonBuf *b, double v);
void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s);
/* JSON-serialize a dyn value into the buffer — the sc_jw_* walker for the
* dyn leaves the compiler cannot type: object members holding undefined
* DROP, array slots holding undefined print null (exactly Node). */
* dyn leaves the compiler cannot type: undefined prints null (an array
* slot's rule); containers follow JSON.stringify (toJSON, accessors,
* circular structures). May leave an exception pending. */
void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d);
/* The entries of record `rec`'s `unknown` index signature (its overflow
* map) after the record's earlier members: separators, keys and values in
* JS own-key order with JSON.stringify's member rules (toJSON called with
* the key, undefined, function and symbol results dropped). `first` is the
* record's first-member flag. May leave an exception pending. */
void scr_jb_put_dyn_entries(ScrJsonBuf *b, void *rec, ScrMap *map, bool *first);
/* Typed JSON for a record that kept a source: the kept key order, declared
* fields through the compiled per-field writer. `try` writes nothing and
* answers false for a record without one. */
+97 -2
View File
@@ -166,6 +166,13 @@ static SCR_TL char **scr_lib_argv_owned = NULL;
static SCR_TL char *scr_lib_argv_utf8 = NULL;
#endif
static SCR_TL bool scr_lib_has_fork = false;
/* A leading Node runtime flag taken out of process.argv (scr_lib_take_flag),
* which process.execArgv reports; NULL without one. */
static SCR_TL const char *scr_lib_exec_flag = NULL;
#if defined(_WIN32) && !defined(SCR_LIB)
/* The flag's index in the original Windows command line, -1 without one. */
static SCR_TL int scr_lib_exec_flag_index = -1;
#endif
#if !defined(_WIN32) && !defined(__wasi__) && !defined(SCR_LIB)
static void scr_title_init(int argc, char **argv);
#endif
@@ -321,6 +328,7 @@ static void scr_lib_prepare_utf8_argv(void) {
// scr_lib_init already consumed this private ASCII fork marker. Do not
// reintroduce it when replacing the narrow argument storage.
if (i == 1 && scr_lib_has_fork) continue;
if (i == scr_lib_exec_flag_index) continue;
int size = WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, NULL, 0, NULL, NULL);
if (size == 0 || WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, next, size, NULL, NULL) != size) {
free(text); free(args); LocalFree(wide);
@@ -565,6 +573,31 @@ const char *scr_lib_arg(int i) {
return scr_lib_argv[i];
}
bool scr_lib_take_flag(const char *flag) {
if (scr_lib_argc < 2 || strcmp(scr_lib_argv[1], flag) != 0) return false;
char **args = malloc((size_t)scr_lib_argc * sizeof(char *));
if (!args) scr_trap("out of memory");
args[0] = scr_lib_argv[0];
for (int i = 2; i < scr_lib_argc; i++) args[i - 1] = scr_lib_argv[i];
args[scr_lib_argc - 1] = NULL;
free(scr_lib_argv_owned);
scr_lib_argv_owned = args;
scr_lib_argv = args;
scr_lib_argc--;
#if defined(_WIN32) && !defined(SCR_LIB)
scr_lib_exec_flag_index = scr_lib_has_fork ? 2 : 1;
#endif
scr_lib_exec_flag = flag;
return true;
}
ScrArr *scr_process_exec_argv(void) {
ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 1);
if (scr_lib_exec_flag)
scr_arr_push_ref(arr, scr_str_new(scr_lib_exec_flag, strlen(scr_lib_exec_flag)));
return arr;
}
bool scr_lib_fork_info(double *target, uintptr_t *read_handle,
uintptr_t *write_handle) {
if (!scr_lib_has_fork) return false;
@@ -10183,6 +10216,8 @@ ScrStr *scr_str_from_code_point_bytes(ScrBytes *codes) {
* frontend-fenced; construction, storage, getters, and ISO formatting
* observe exactly this payload. */
SCR_TL const double *scr_date_mock_now = NULL;
double scr_date_now(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
@@ -10873,7 +10908,7 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho
ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) {
double ms;
if (arguments->v.arr.len == 0) ms = scr_date_now();
if (arguments->v.arr.len == 0) ms = scr_date_mock_now ? *scr_date_mock_now : scr_date_now();
else {
ScrDyn *value = arguments->v.arr.items[0];
if (scr_dyn_native_date_is(value)) ms = scr_dyn_native_date_value(value);
@@ -10881,9 +10916,69 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) {
else if (!scr_dyn_number_coerce_js(value, &ms)) return NULL;
ms = scr_date_new_ms(ms);
}
return scr_dyn_native_date_from_ms(ms);
}
/* The Date.prototype methods scr_native_date_invoke answers. */
static const char *const scr_native_date_methods[] = {
"getTime", "valueOf", "toISOString", "toJSON", "getFullYear", "getUTCFullYear", "getMonth",
"getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours",
"getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds",
"getUTCMilliseconds", "getTimezoneOffset",
};
#define SCR_NATIVE_DATE_METHODS (sizeof scr_native_date_methods / sizeof scr_native_date_methods[0])
static SCR_TL ScrDyn *scr_native_date_method_values[SCR_NATIVE_DATE_METHODS];
static SCR_TL bool scr_native_date_methods_registered;
static void scr_native_date_methods_cleanup(void) {
for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS; i++) {
ScrDyn *function = scr_native_date_method_values[i];
scr_native_date_method_values[i] = NULL;
scr_dyn_release(function);
}
}
/* A Date method read as a value and called later: the closure's function
* slot holds the method's name, and the receiver must be a Date. */
static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
const char *method = closure->fn;
ScrDyn *self = scr_dyn_this_get();
ScrDyn *result = NULL;
if (scr_dyn_native_date_is(self)) {
result = scr_native_date_invoke(self->v.handle.ptr, self, method, args, argc, method);
} else {
static const char message[] = "this is not a Date object.";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
}
scr_dyn_release(self);
return result;
}
/* Property reads of a Date: its methods as function values, one per name
* and thread, and NULL (undefined) for other names. */
static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) {
(void)ptr;
for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS; i++) {
const char *name = scr_native_date_methods[i];
if (strlen(name) != key_len || memcmp(name, key, key_len) != 0) continue;
if (!scr_native_date_methods_registered) {
scr_native_date_methods_registered = true;
scr_atexit(scr_native_date_methods_cleanup);
}
if (!scr_native_date_method_values[i]) {
scr_native_date_method_values[i] = scr_dyn_new_func(
scr_closure_new((void *)name, 0), scr_native_date_method_call, 0, "native:Date", name);
}
return scr_dyn_retain(scr_native_date_method_values[i]);
}
return NULL;
}
/* A Date handle holding the time value `ms` (already TimeClip'd). */
ScrDyn *scr_dyn_native_date_from_ms(double ms) {
static const ScrDynHandleOps ops = {
"Date", &scr_native_date_retain, &scr_native_date_release, &scr_native_date_invoke,
NULL, NULL, NULL, NULL,
&scr_native_date_get, NULL, NULL, NULL,
};
scr_dyn_handle_install(SCR_DYNH_DATE, &ops);
ScrNativeDate *date = malloc(sizeof *date);
+7
View File
@@ -46,6 +46,13 @@ ScrArr *scr_module_cache_keys(void);
int scr_lib_arg_count(void);
const char *scr_lib_arg(int i);
/* Takes a leading `flag` argument out of process.argv, as Node keeps its
* runtime flags out of it, and reports it in process.execArgv. Called
* right after scr_lib_init, before process.argv is first read. True when
* the flag was there. */
bool scr_lib_take_flag(const char *flag);
/* process.execArgv: the flag scr_lib_take_flag took, or [] (+1, fresh). */
ScrArr *scr_process_exec_argv(void);
/* Private self-reexec marker parsed and removed from process.argv by
* scr_lib_init. False in an ordinary parent invocation. */
bool scr_lib_fork_info(double *target, uintptr_t *read_handle,
+7
View File
@@ -265,6 +265,10 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
case SCR_DYN_HANDLE: identity = input->v.handle.ptr; domain = 4; break;
default: break;
}
if (scr_uncloneable_count && scr_uncloneable_marked(input)) {
scr_throw_domex("DataCloneError", "Cannot clone object of unsupported type.");
return SIZE_MAX;
}
size_t found = scr_message_find(encoder, identity, domain);
if (found != SIZE_MAX) return found;
size_t id = scr_message_node(encoder);
@@ -274,6 +278,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
* capsule over the same object, a dyn graph as itself. */
if (input->kind == SCR_DYN_TYPED_REF && input->v.typed_ref.ptr &&
*(const size_t *)input->v.typed_ref.ptr == SIZE_MAX) {
scr_sa_thread_shares();
encoder->visits[id].view = (ScrDyn *)input;
ScrMessageNode *node = &encoder->message->nodes[id];
node->kind = SCR_MESSAGE_TYPED_REF;
@@ -291,6 +296,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
return id;
}
if ((input->kind == SCR_DYN_OBJ || input->kind == SCR_DYN_ARR) && input->rc == SIZE_MAX) {
scr_sa_thread_shares();
encoder->visits[id].view = (ScrDyn *)input;
ScrMessageNode *node = &encoder->message->nodes[id];
node->kind = SCR_MESSAGE_PUBLISHED;
@@ -554,6 +560,7 @@ static ScrMessage *scr_message_try_publish(const ScrDyn *value) {
scr_lsan_immortal(d);
}
}
scr_sa_thread_shares();
message = scr_message_alloc(1, sizeof(*message));
message->nodes = scr_message_alloc(1, sizeof(*message->nodes));
message->count = message->capacity = 1;
+4
View File
@@ -91,6 +91,10 @@ ScrStr *scr_intl_num_format_en_us(double x);
* mutation are frontend-fenced; construction, storage, getters, and ISO
* formatting are exact over this representation. */
double scr_date_now(void); /* integer ms since epoch, like Node */
/* The clock of the MockTimers that mocks Date (scr_test.c), NULL while
* Date is not mocked. Dynamic `new Date()` reads it; the runtime's own
* time stamps keep scr_date_now, as Node's internals keep the real Date. */
extern SCR_TL const double *scr_date_mock_now;
double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */
double scr_date_get_time(double ms);
/* Node's exact ISO 8601 UTC format (expanded ±YYYYYY years outside
+8 -1
View File
@@ -341,11 +341,18 @@ void scr_sa_thread_park(void);
/* Waits until the arenas of exited threads are trimmed (tests). */
void scr_sa_wait_trims(void);
/* Called first on a new worker thread: prepares the thread's system-
* allocator heap (scr_alloc.c). */
* allocator heap, and lets the thread's memory go back when it exits
* (scr_alloc.c). */
void scr_sa_thread_starting(void);
/* The calling thread made objects it allocated reachable from other
* threads: a message or an intern table shares a published graph by
* pointer, or a process-wide cache keeps an immortal object. Its memory
* then stays when it exits (scr_alloc.c). */
void scr_sa_thread_shares(void);
#define SCR_SA_FREE_FOREIGN(p) scr_sa_free_foreign(p)
#else
#define SCR_SA_FREE_FOREIGN(p) free(p)
#define scr_sa_thread_shares() ((void)0)
#endif
#if SCR_SMALL_ALLOC
+5
View File
@@ -90,6 +90,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) {
scr_dyn_install_async_iterator_symbol(key);
scr_dyn_release(key);
}
if (name->len == 12 && !memcmp(name->data, "asyncDispose", 12)) {
ScrDyn *key = scr_dyn_new_symbol(symbol);
scr_dyn_install_async_dispose_symbol(key);
scr_dyn_release(key);
}
if (name->len == 11 && !memcmp(name->data, "toPrimitive", 11)) {
ScrDyn *key = scr_dyn_new_symbol(symbol);
scr_dyn_install_to_primitive_symbol(key);
File diff suppressed because it is too large Load Diff
+52
View File
@@ -57,6 +57,58 @@ void scr_test_mock_reset(void);
void scr_test_ctx_mock_track(ScrTestCtx *t, ScrClosure *restore);
void scr_test_ctx_mock_restore_all(ScrTestCtx *t);
void scr_test_ctx_mock_reset(ScrTestCtx *t);
/* Called by node:test programs right after scr_lib_init: a leading
* --test-update-snapshots argument, Node's flag, turns on snapshot update
* mode and moves from process.argv to process.execArgv. */
void scr_test_init(void);
/* t.assert.snapshot and t.assert.fileSnapshot. snapshot_value escapes
* and wraps a serialized value (+1); ctx_snapshot returns the stored value
* for the test's next snapshot id in `file` and file_snapshot the
* contents of `path` (+1), or the recorded value in update mode. Both
* throw Node's ERR_INVALID_STATE for missing or unreadable snapshots and
* return NULL then. */
ScrStr *scr_test_snapshot_value(ScrStr *serialized);
ScrDyn *scr_test_snapshot_serialize(ScrDyn *value, ScrDyn *serializers);
ScrStr *scr_test_ctx_snapshot(ScrTestCtx *t, ScrStr *value, ScrStr *file);
ScrStr *scr_test_file_snapshot(ScrStr *value, ScrStr *path);
/* mock.timers on the module's `mock` and on t.mock: enable({ apis, now })
* (`apis` one code per listed API: 'T' setTimeout, 'I' setInterval, 'M'
* setImmediate, 'D' Date, 'W' scheduler.wait, 'A' AbortSignal.timeout;
* flags 1 = `now` given, 2 = it came from a Date), tick, runAll, reset
* and setTime. enable, tick, runAll and setTime throw Node's
* ERR_INVALID_STATE and ERR_INVALID_ARG_VALUE errors, and tick and runAll
* run the due timer callbacks, whose throws propagate. */
void scr_test_mock_timers_enable(ScrStr *apis, double now, double flags);
void scr_test_ctx_mock_timers_enable(ScrTestCtx *t, ScrStr *apis, double now, double flags);
void scr_test_mock_timers_tick(double time);
void scr_test_ctx_mock_timers_tick(ScrTestCtx *t, double time);
void scr_test_mock_timers_run_all(void);
void scr_test_ctx_mock_timers_run_all(ScrTestCtx *t);
void scr_test_mock_timers_reset(void);
void scr_test_ctx_mock_timers_reset(ScrTestCtx *t);
void scr_test_mock_timers_set_time(double time);
void scr_test_ctx_mock_timers_set_time(ScrTestCtx *t, double time);
/* The timer calls of programs that use mock.timers (moduleUsesMockTimers):
* each queues on the API's MockTimers owner when it has one, else calls
* the loop's function of the same shape. */
void scr_test_mt_set_timeout(ScrClosure *cb, double ms);
double scr_test_mt_set_timeout_handle(ScrClosure *cb, double ms);
double scr_test_mt_set_interval(ScrClosure *cb, double ms);
void scr_test_mt_clear_timeout(double handle);
void scr_test_mt_clear_interval(double handle);
void scr_test_mt_unref(double handle);
void scr_test_mt_ref(double handle);
void scr_test_mt_refresh(double handle);
bool scr_test_mt_has_ref(double handle);
double scr_test_mt_set_immediate(ScrClosure *cb);
void scr_test_mt_clear_immediate(double handle);
void scr_test_mt_immediate_unref(double handle);
void scr_test_mt_immediate_ref(double handle);
bool scr_test_mt_immediate_has_ref(double handle);
double scr_test_mt_date_now(void);
/* timers/promises scheduler.wait(delay) (+1 promise; NULL after a throw). */
ScrPromise *scr_test_mt_scheduler_wait(double ms);
ScrPromise *scr_test_mt_scheduler_wait_value(double ms, ScrPromise *value, bool ref);
/* Main's epilogue (emitted when moduleUsesNodeTest): 1 when any non-todo
* test failed, Node's exit contract. */
int scr_test_exit_code(void);
+39
View File
@@ -10,6 +10,7 @@ typedef struct ScrWorkerView {
ScrEmitter *events;
ScrWorkerThread *thread;
ScrPromise *termination;
ScrPromise *disposal; /* worker[Symbol.asyncDispose]()'s promise */
bool port;
bool closed;
bool referenced;
@@ -130,6 +131,7 @@ static void scr_worker_view_trace(void *ptr, ScrTraceVisit visit, void *ctx) {
ScrWorkerView *view = ptr;
visit(view->events, ctx);
visit(view->termination, ctx);
visit(view->disposal, ctx);
for (int i = 0; i < 2; i++) {
visit(view->stdio[i], ctx);
visit(view->stdio_box[i], ctx);
@@ -158,6 +160,7 @@ static void scr_worker_view_destroy(void *ptr) {
ScrWorkerView *view = ptr;
scr_emitter_release(view->events);
scr_promise_release(view->termination);
scr_promise_release(view->disposal);
for (int i = 0; i < 2; i++) {
if (view->stdio[i]) scr_worker_stdio_ops->release(view->stdio[i]);
scr_dyn_release(view->stdio_box[i]);
@@ -457,6 +460,39 @@ static ScrDyn *scr_worker_method_call(ScrClosure *closure, ScrDyn *const *args,
return result;
}
/* worker[Symbol.asyncDispose](): Node's `await this.terminate()`, a
* promise of undefined once the worker exited. */
static ScrDyn *scr_worker_async_dispose_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
(void)args;
(void)argc;
ScrDyn *self = scr_dyn_this_get();
ScrDyn *result = NULL;
if (self->kind != SCR_DYN_HANDLE || self->v.handle.tag != SCR_DYNH_WORKER) {
static const char message[] = "Illegal invocation";
scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_THIS");
} else {
ScrWorkerView *view = self->v.handle.ptr;
if (!view->disposal) {
view->disposal = scr_promise_new();
if (view->closed) scr_promise_fulfill_ref(view->disposal,
scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
else if (!view->termination) scr_worker_thread_terminate(view->thread);
}
result = scr_dyn_new_promise(view->disposal);
}
scr_dyn_release(self);
return result;
}
/* worker[Symbol.asyncDispose] */
static ScrDyn *scr_worker_symbol_get(void *ptr, const ScrDyn *key) {
(void)ptr;
if (!scr_dyn_is_async_dispose_symbol(key)) return NULL;
ScrClosure *closure = scr_closure_new((void *)&scr_worker_async_dispose_call, 0);
return scr_dyn_new_func(closure, &scr_worker_async_dispose_call, 0, "()", "[Symbol.asyncDispose]");
}
static ScrDyn *scr_worker_view_get(void *ptr, const char *key, size_t length) {
ScrWorkerView *view = ptr;
if (!view->port && length == 8 && !memcmp(key, "threadId", 8))
@@ -735,6 +771,8 @@ static void scr_worker_dispatch(void) {
view->closed = true;
if (view->termination) scr_promise_fulfill_ref(view->termination,
scr_dyn_new_num(event->code), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
if (view->disposal) scr_promise_fulfill_ref(view->disposal,
scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
ScrWorkerView **slot = &scr_worker_views;
while (*slot != view) slot = &(*slot)->next;
*slot = view->next;
@@ -819,6 +857,7 @@ static bool scr_worker_events_install(void) {
static const ScrDynHandleOps worker_ops = {
.cls = "Worker", .retain = scr_worker_view_retain, .release = scr_worker_view_release,
.invoke = scr_worker_view_invoke, .get = scr_worker_view_get,
.symbol_get = scr_worker_symbol_get,
};
static const ScrDynHandleOps port_ops = {
.cls = "MessagePort", .retain = scr_worker_view_retain, .release = scr_worker_view_release,
+79
View File
@@ -1187,6 +1187,84 @@ static void check_large_old_buffer_with_nursery_candidates(void) {
}
}
/* The death rule: once objects have died outside passes (the live count
* fell by 1/32 of the heap the last full pass left, here the floor's), a
* full pass is due as soon as the heap has grown the base quarter past it,
* however wide the growth fraction has become. With the old level's
* fraction at the cap, the dead old ring below would otherwise wait until
* the heap had grown to the cap. A fall that the pass it brings finds
* nothing behind doubles the fall the next one needs. */
static size_t watched_freed;
static void watched_free(void *obj) {
watched_freed++;
node_free(obj);
}
/* Fresh live leaves that are never released, each followed by a dead
* one-node ring, which keeps the release path running its passes without
* buffering the leaves, until the watched ring is freed or `limit` leaves
* have been made. Returns how many it made. */
static size_t grow_until_watched(Node **keep, size_t limit, size_t watched) {
size_t n = 0;
while (watched_freed < watched && n < limit) {
keep[n++] = make_leaf();
release_live(make_ring(1));
}
return n;
}
static void check_death_brings_full_pass(void) {
enum { FLOOR = 4096, RING = 64, LEAVES = 1024 };
static ScrCycType watched_type = { &node_trace, &watched_free, 0 };
size_t cap = configured_growth_cap();
if (cap < 4) return; /* growth alone is due about as soon */
size_t limit = FLOOR + FLOOR / 4 + 2 * configured_nursery_threshold() + 2;
Node **leaves = calloc(LEAVES, sizeof(*leaves));
Node **keep = calloc(limit, sizeof(*keep));
if (!leaves || !keep) scr_trap("cycle test: out of memory\n");
/* Growth passes that find nothing widen the old fraction to the cap. */
for (int i = 0; i < 4; i++) grow_dead_cycle_until_full_pass(0, false);
Node *ring = make_ring_of(RING, &watched_type);
age_to_old(ring);
for (size_t i = 0; i < LEAVES; i++) leaves[i] = make_leaf();
scr_collect_cycles();
check(scr_cyc_live < FLOOR - LEAVES, "live heap did not start below the growth floor");
watched_freed = 0;
drop_leaves(leaves, LEAVES); /* gone by reference counting: the live count falls */
scr_cyc_collect_scheduled(); /* a check reads the whole fall */
release_live(ring); /* a dead old cycle */
size_t grown = grow_until_watched(keep, limit, RING);
check(watched_freed == RING, "the fall did not bring a full pass by the base fraction");
drop_leaves(keep, grown);
scr_collect_cycles();
/* A fall with nothing behind it: the pass it brings finds nothing. */
for (size_t i = 0; i < LEAVES; i++) leaves[i] = make_leaf();
scr_collect_cycles();
drop_leaves(leaves, LEAVES);
scr_cyc_collect_scheduled();
grown = grow_until_watched(keep, limit, 1);
drop_leaves(keep, grown);
/* So the next death pass needs twice the fall: 1/32 of the floor, enough
* before, no longer is. */
ring = make_ring_of(RING, &watched_type);
age_to_old(ring);
for (size_t i = 0; i < FLOOR / 32; i++) leaves[i] = make_leaf();
scr_collect_cycles();
watched_freed = 0;
drop_leaves(leaves, FLOOR / 32);
scr_cyc_collect_scheduled();
release_live(ring);
grown = grow_until_watched(keep, limit, RING);
check(watched_freed == 0, "a fall smaller than the backed-off one brought a full pass");
drop_leaves(keep, grown);
scr_collect_cycles();
check(watched_freed == RING, "the full sweep did not reclaim the dead old ring");
free(leaves);
free(keep);
}
int main(void) {
check_old_garbage_reclaimed_while_growing();
check_older_rings_into_old();
@@ -1207,6 +1285,7 @@ int main(void) {
check_growth_schedule_follows_yield();
check_growth_cap_bounds_floating_garbage();
check_mixed_dead_structure_narrows();
check_death_brings_full_pass();
check_nursery_bypass();
check_mature_bypass();
check_probe_sees_candidate_rings();
+143
View File
@@ -0,0 +1,143 @@
// Removing the last element with a statement-level pop() or `length = n`,
// for every element kind, on dense arrays, arrays with holes and present
// undefined values, sparse arrays and arrays shared between bindings.
// Removed references are released; the remaining elements, the length and
// later appends match Node.
class Item {
readonly id: number;
constructor(id: number) {
this.id = id;
}
}
function show(label: string, values: unknown[]): void {
const parts: string[] = [];
for (let i = 0; i < values.length; i++) {
if (!(i in values)) parts.push("<hole>");
else {
const v = values[i];
parts.push(v instanceof Item ? `I${v.id}` : (JSON.stringify(v) ?? "undefined"));
}
}
console.log(label, values.length, parts.join(" "));
}
const numbers = [1.5, 2.5, 3.5, 4.5];
numbers.pop();
numbers.length = numbers.length - 1;
numbers.push(9);
show("numbers", numbers);
const flags = [true, false, true];
flags.pop();
flags.length = 1;
flags.push(false);
show("flags", flags);
const words = ["a", "b", "c", "d"];
const alias = words;
words.pop();
alias.length = alias.length - 1;
words.push("z");
show("words", words);
show("alias", alias);
const items = [new Item(1), new Item(2), new Item(3)];
const kept = items[2]!;
items.pop();
items.length = 1;
items.push(new Item(4));
show("items", items);
console.log("kept", kept.id);
// Holes and present undefined at the end.
const holes: (number | undefined)[] = [1, 2];
holes[4] = 5;
holes.length = 4;
show("holes trimmed", holes);
holes.length = 3;
show("holes trimmed again", holes);
const undef: (string | undefined)[] = ["x", undefined, undefined];
undef.pop();
undef.length = undef.length - 1;
show("undefined", undef);
// A sparse array: a far index then shrinking past it and back by one.
const sparse: number[] = [1, 2, 3];
sparse[2000000] = 4;
sparse.length = 2000000;
show("sparse cut", sparse.slice(0, 4));
console.log("sparse length", sparse.length, 1999999 in sparse);
sparse.length = sparse.length - 1;
console.log("sparse length", sparse.length);
// A stack used the way a checker pushes and pops contextual entries.
const stack: Item[] = [];
const top: number[] = [];
let checksum = 0;
for (let i = 0; i < 20000; i++) {
stack.push(new Item(i));
top.push(i);
if (i % 3 === 2) {
stack.pop();
top.length = top.length - 1;
}
if (stack.length > 50) {
stack.length = stack.length - 1;
top.pop();
}
checksum = (checksum + stack[stack.length - 1]!.id + top[top.length - 1]!) % 1000003;
}
console.log("stack", stack.length, top.length, checksum);
// Lengths that are not one less: unchanged, longer, invalid.
const other = [1, 2, 3];
other.length = 3;
other.length = 5;
show("longer", other);
other.length = 1;
show("shorter by many", other);
try {
other.length = -1;
} catch (e) {
console.log("invalid", (e as Error).name, other.length);
}
try {
other.length = 0.5;
} catch (e) {
console.log("fraction", (e as Error).name, other.length);
}
const empty: number[] = [];
empty.pop();
empty.length = 0;
show("empty", empty);
// pop() results: the last element, undefined for an empty array, a hole or a
// present undefined, and values taken from sparse storage.
const nums = [0.5, -0, 7];
console.log("pop number", nums.pop(), Object.is(nums.pop(), -0), nums.pop(), nums.pop(), nums.length);
const bools = [false, true];
console.log("pop bool", bools.pop(), bools.pop(), bools.pop(), bools.length);
const strs = ["p", "q"];
const strAlias = strs;
console.log("pop string", strs.pop(), strAlias.pop(), strs.pop(), strAlias.length);
const objs = [new Item(7), new Item(8)];
const popped = objs.pop();
console.log("pop item", popped?.id, objs.pop()?.id, objs.pop(), objs.length);
const gaps: number[] = [1];
gaps[3] = 4;
console.log("pop gaps", gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.length);
const maybe: (number | undefined)[] = [1, undefined];
console.log("pop present undefined", maybe.pop(), maybe.length, maybe.pop(), maybe.length);
const far: string[] = ["a"];
far[100000] = "b";
console.log("pop sparse", far.pop(), far.length, far.pop(), far.length);
let popTotal = 0;
const work: number[] = [];
for (let i = 0; i < 10000; i++) {
work.push(i, i + 1);
popTotal += work.pop()!;
if (i % 5 === 0) popTotal += work.pop() ?? -1;
}
console.log("pop loop", work.length, popTotal);
+91
View File
@@ -0,0 +1,91 @@
// The module's mock.timers outside a test: a debounce and a retry loop
// driven by tick and runAll, the order due timers run in, intervals that
// clear themselves, setTime, Node's errors, and real timers made before
// enable() that keep running through the loop.
import { mock } from "node:test";
function debounce(fn: (value: string) => void, ms: number): (value: string) => void {
let pending: ReturnType<typeof setTimeout> | null = null;
return (value: string) => {
if (pending !== null) clearTimeout(pending);
pending = setTimeout(() => {
pending = null;
fn(value);
}, ms);
};
}
const real = setTimeout(() => console.log("real timer fired"), 1);
mock.timers.enable({ apis: ["setTimeout", "setInterval"] });
clearTimeout(real); // the mocked clearTimeout leaves the real timer armed
const save = debounce((value) => console.log(`saved ${value}`), 100);
save("a");
mock.timers.tick(50);
save("ab");
mock.timers.tick(99);
console.log("nothing saved yet");
save("abc");
mock.timers.tick(100);
let attempts = 0;
function attempt(delay: number): void {
attempts++;
console.log(`attempt ${attempts} after ${delay}ms`);
if (attempts < 4) setTimeout(() => attempt(delay * 2), delay * 2);
}
setTimeout(() => attempt(10), 10);
mock.timers.runAll();
console.log(`runAll stopped after ${attempts} attempts`);
mock.timers.runAll();
mock.timers.runAll();
mock.timers.runAll();
console.log(`attempts ${attempts}`);
const order: string[] = [];
setTimeout(() => order.push("omitted"));
setTimeout(() => order.push("zero"), 0);
setTimeout(() => order.push("negative"), -1);
setTimeout((tag: string) => order.push(tag), 0, "zero with argument");
mock.timers.tick(0);
console.log(order.join(", "));
let beats = 0;
const heartbeat = setInterval(() => {
beats++;
if (beats === 5) clearInterval(heartbeat);
}, 30);
mock.timers.tick(1000);
console.log(`beats ${beats}`);
const handle = setTimeout(() => console.log("refreshed timer fired once"), 20);
console.log(handle.hasRef(), handle.unref().hasRef(), handle.ref().hasRef());
handle.refresh();
mock.timers.tick(20);
mock.timers.tick(20);
mock.timers.setTime(1_000_000);
setTimeout(() => console.log("fires after setTime"), 1);
mock.timers.tick(1);
for (const run of [
() => mock.timers.enable({ apis: ["setTimeout"] }),
() => mock.timers.tick(-2),
() => mock.timers.setTime(-0.5),
]) {
try {
run();
} catch (error) {
const e = error as { name: string; code: string; message: string };
console.log(`${e.name} ${e.code}: ${e.message}`);
}
}
mock.timers.reset();
try {
mock.timers.runAll();
} catch (error) {
console.log((error as Error).message);
}
setTimeout(() => console.log("real again after reset"), 30);
+83
View File
@@ -0,0 +1,83 @@
// The module's mock.timers for Date, setImmediate and AbortSignal.timeout:
// a cache whose entries expire by Date.now(), a batch that flushes on
// setImmediate, a request deadline from AbortSignal.timeout, and
// enable() without options, which mocks every API.
import { mock } from "node:test";
class TtlCache {
private entries = new Map<string, { value: string; expires: number }>();
private ttl: number;
constructor(ttl: number) {
this.ttl = ttl;
}
set(key: string, value: string): void {
this.entries.set(key, { value, expires: Date.now() + this.ttl });
}
get(key: string): string | undefined {
const entry = this.entries.get(key);
if (entry === undefined) return undefined;
if (Date.now() >= entry.expires) {
this.entries.delete(key);
return undefined;
}
return entry.value;
}
}
mock.timers.enable({ apis: ["Date"], now: new Date("2024-05-01T12:00:00Z") });
console.log(new Date().toISOString());
const cache = new TtlCache(60_000);
cache.set("user", "ada");
mock.timers.tick(59_999);
console.log(cache.get("user"));
mock.timers.tick(1);
console.log(cache.get("user"));
mock.timers.setTime(0);
console.log(Date.now(), new Date().toISOString());
mock.timers.reset();
console.log(Date.now() > 1_700_000_000_000);
const batches: string[][] = [];
let pending: string[] = [];
function queue(item: string): void {
if (pending.length === 0) {
setImmediate(() => {
batches.push(pending);
pending = [];
});
}
pending.push(item);
}
mock.timers.enable({ apis: ["setImmediate"] });
queue("a");
queue("b");
mock.timers.tick(0);
console.log(JSON.stringify(batches));
queue("c");
queue("d");
console.log(JSON.stringify(pending));
mock.timers.tick(0);
console.log(JSON.stringify(batches));
const skipped = setImmediate(() => console.log("never"));
clearImmediate(skipped);
console.log(skipped.hasRef());
mock.timers.runAll();
mock.timers.reset();
mock.timers.enable();
const deadline = AbortSignal.timeout(5_000);
deadline.addEventListener("abort", () => {
const reason = deadline.reason as { name: string; message: string };
console.log(`aborted at ${Date.now()}: ${reason.name}: ${reason.message}`);
});
mock.timers.tick(4_999);
console.log(deadline.aborted);
mock.timers.tick(1);
console.log(deadline.aborted);
mock.timers.reset();
const real = AbortSignal.timeout(1);
real.addEventListener("abort", () => {
console.log(`real deadline: ${(real.reason as { name: string }).name}`);
});
@@ -0,0 +1,76 @@
// JSON.stringify over typed records whose members are checked-dynamic
// values: the undeclared properties a checked record keeps from its source,
// and the entries of an `unknown` index signature. Each member follows
// SerializeJSONProperty: toJSON runs with the member's key, getters are
// read, a Buffer becomes its { type, data } form, undefined, function and
// symbol results drop, and a circular structure throws V8's TypeError.
type Point = { a: number };
type Bag = { a: number; [key: string]: unknown };
const report = (label: string, run: () => string | undefined): void => {
try {
console.log(label, run());
} catch (error) {
const e = error as Error;
console.log(label, e.name);
console.log(e.message);
}
};
const source: any = JSON.parse('{"a":1,"b":"two"}');
source.stamp = { toJSON: (key: string) => `stamp:${key}` };
source.gone = { toJSON: () => undefined };
source.bytes = Buffer.from("hi");
Object.defineProperty(source, "computed", { get: () => 42, enumerable: true });
source.list = [1, { toJSON: (key: string) => `item:${key}` }];
const point = source as Point;
report("source", () => JSON.stringify(point));
report("source pretty", () => JSON.stringify(point, null, 2));
report("source nested", () => JSON.stringify({ point, n: 2 }));
report("source array", () => JSON.stringify([point, point]));
const bag: Bag = { a: 1 };
bag.stamp = { toJSON: (key: string) => `stamp:${key}` };
bag.gone = { toJSON: () => undefined };
bag.data = JSON.parse('{"x":[1,2,{"y":null}]}');
bag.nothing = undefined;
bag.order = JSON.parse('{"b":1,"2":"two","a":2,"0":"zero"}');
const layered: any = JSON.parse('{"level":{"items":[1,{"x":null}]}}');
layered.level.items.push({ toJSON: (key: string) => `deep:${key}` });
bag.deep = layered;
const hidden: any = JSON.parse('{"shown":1}');
Object.defineProperty(hidden, "hidden", { value: 2, enumerable: false });
bag.hidden = hidden;
let nested: any = JSON.parse('{"end":true}');
for (let i = 0; i < 40; i++) nested = { i, nested };
bag.nested = nested;
report("bag", () => JSON.stringify(bag));
report("bag in array", () => JSON.stringify([bag]));
const self: any = JSON.parse('{"k":1}');
self.self = self;
const cyclic: any = JSON.parse('{"a":1,"x":{"k":2}}');
cyclic.x.self = cyclic.x;
report("source circular", () => JSON.stringify(cyclic as Point));
const deep: any = JSON.parse('{"a":1,"x":{"y":{"z":{"w":{"v":{}}}}}}');
deep.x.y.z.w.v.back = deep.x;
report("source deep circular", () => JSON.stringify(deep as Point));
const circular: Bag = { a: 1 };
circular.loop = self;
report("bag circular", () => JSON.stringify(circular));
report("bag circular nested", () => JSON.stringify({ outer: circular }));
const shared: any = JSON.parse('{"v":1}');
const twice: Bag = { a: 1, first: shared, second: shared };
report("bag shared", () => JSON.stringify(twice));
const big: any = JSON.parse('{"a":1}');
big.n = 1n;
report("source bigint", () => JSON.stringify(big as Point));
const throwing: Bag = { a: 1 };
throwing.bad = {
toJSON: () => {
throw new RangeError("from toJSON");
},
};
report("bag throwing", () => JSON.stringify(throwing));
+57
View File
@@ -0,0 +1,57 @@
// Date values converted into `unknown` slots stay Dates: typeof answers
// "object", instanceof Date holds, util.inspect prints the ISO form or
// "Invalid Date", JSON.stringify calls toJSON (null for an invalid date),
// a method read through an untyped reference is a function that takes a
// Date receiver, and the value converts back to a typed Date with the same
// time value.
import { inspect } from "node:util";
const epoch = new Date(0);
const bare: unknown = epoch;
const record: unknown = { when: new Date(86_400_000), n: 1 };
const list: unknown = [new Date(Date.UTC(2024, 1, 29, 12, 30)), new Date(Number.NaN)];
const bag: Record<string, unknown> = { at: epoch };
console.log(typeof bare, bare instanceof Date, inspect(bare), JSON.stringify(bare));
console.log(record, JSON.stringify(record), Object.keys(record as object));
console.log(list, JSON.stringify(list));
console.log(bag, JSON.stringify(bag));
const when = (record as { when: unknown }).when;
console.log(typeof when, when instanceof Date);
if (when instanceof Date) console.log(when.getTime(), when.toISOString(), when.getUTCDate());
const back = bare as Date;
console.log(back.getTime(), back.toISOString());
const dynamic = record as any;
console.log(dynamic.when.getTime(), dynamic.when.getUTCFullYear(), String(dynamic.when instanceof Date));
const read = dynamic.when.getUTCHours;
console.log(typeof read, read.name, read.call(dynamic.when), dynamic.when.missing);
try {
read.call({});
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
type Event = { at: Date; tags: string[] };
const events: Event[] = [
{ at: new Date(0), tags: ["a"] },
{ at: new Date(7), tags: [] },
];
const eventsUnknown: unknown = events;
console.log(eventsUnknown, JSON.stringify(eventsUnknown));
console.log((eventsUnknown as Event[])[1]!.at.toISOString());
class Holder {
value: unknown = null;
}
const holder = new Holder();
holder.value = new Date(1000);
console.log(holder, JSON.stringify(holder.value));
const invalid: any = { d: new Date(Number.NaN) } as unknown;
try {
console.log(invalid.d.toISOString());
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
console.log(invalid.d.getTime(), JSON.stringify(invalid));
const stamped: Readonly<Date> = new Date(0);
console.log(stamped.getTime(), stamped.toISOString());
@@ -0,0 +1,28 @@
// events.defaultMaxListeners through the module's ES module namespace and
// named imports reads the namespace's copy, which keeps the value from
// program start until module.syncBuiltinESMExports() refreshes it. The
// default import and the EventEmitter class read the current default.
import EventEmitterDefault, { defaultMaxListeners, EventEmitter, setMaxListeners } from "node:events";
import * as events from "node:events";
import { syncBuiltinESMExports } from "node:module";
const show = (label: string): void => {
console.log(
label,
events.defaultMaxListeners,
defaultMaxListeners,
EventEmitterDefault.defaultMaxListeners,
EventEmitter.defaultMaxListeners,
);
};
show("start");
setMaxListeners(20);
show("after setMaxListeners");
EventEmitter.defaultMaxListeners = 7;
show("after assignment");
syncBuiltinESMExports();
show("after sync");
events.setMaxListeners(3);
show("after namespace setMaxListeners");
const emitter = new EventEmitter();
console.log(emitter.getMaxListeners(), events.getMaxListeners(emitter));
@@ -0,0 +1,10 @@
// CommonJS bindings of node:events read defaultMaxListeners from the module
// object, which is the EventEmitter class: every read sees the current
// default.
const events = require("node:events");
const { EventEmitter, setMaxListeners } = require("node:events");
console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners);
setMaxListeners(15);
console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners, require("events").defaultMaxListeners);
events.defaultMaxListeners = 4;
console.log(events.defaultMaxListeners, EventEmitter.defaultMaxListeners, new EventEmitter().getMaxListeners());
@@ -0,0 +1,48 @@
// Errors held as `any` convert to strings through Error.prototype.toString
// in every dynamic conversion: template literals, `+` with a string on
// either side, String(), Array join, loose equality, Number() and property
// keys. The name is read through the prototype ("Error", "TypeError", a
// subclass's own name), an empty name or message leaves the other alone,
// and a subclass without its own name inherits "Error".
class NamedError extends Error {
constructor(message: string) {
super(message);
this.name = "NamedError";
}
}
class PlainSubclass extends Error {}
function show(label: string, error: any): void {
console.log(label, "|", `${error}`, "|", "" + error, "|", error + "!", "|", String(error), "|", [error, 1].join(" "));
}
show("error", new Error("plain"));
show("type", new TypeError("bad thing"));
show("range", new RangeError("too far"));
show("syntax", new SyntaxError("unexpected"));
show("named", new NamedError("mine"));
show("subclass", new PlainSubclass("inherited name"));
show("empty message", new TypeError(""));
show("no message", new Error());
const renamed: any = new Error("message only");
renamed.name = "";
show("empty name", renamed);
const changed: any = new RangeError("before");
changed.message = "after";
show("changed message", changed);
const numeric: any = new Error("42");
numeric.name = "";
const unnumbered: any = new TypeError("x");
console.log("Number", Number(numeric), Number(unnumbered), +numeric);
const type: any = new TypeError("bad thing");
console.log("loose equality", type == "TypeError: bad thing", type == "bad thing");
const keyed: any = {};
keyed[type] = 1;
console.log("key", Object.keys(keyed).join(","));
@@ -0,0 +1,9 @@
// typeof on functions and classes exported by Node builtin modules, read
// through require bindings: Node answers "function" for each.
const zlib = require("node:zlib");
const path = require("node:path");
const fs = require("node:fs");
console.log(typeof zlib.createGzip, typeof zlib.createBrotliDecompress, typeof zlib.gunzipSync);
console.log(typeof zlib.Inflate, typeof zlib.Gzip, typeof zlib.DeflateRaw);
console.log(typeof path.join, typeof path.posix.join, typeof fs.readFileSync);
console.log(typeof path.sep);

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