mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +08:00
Merge main
This commit is contained in:
@@ -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. On Linux, the instances a worker thread creates are returned with the rest of its memory when it exits, unless the worker shared objects with other threads (see [cost](/docs/threads#cost)).
|
||||
`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.
|
||||
|
||||
```ts:tree.ts
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
|
||||
@@ -234,8 +234,6 @@ 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. 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 @@
|
||||
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `d` (match indices) and `v` (unicode sets) flags are unsupported: regex literals and `new RegExp` calls with a literal flag string containing them fail to compile with SC1120, and `new RegExp` with a computed flag string containing them throws an error naming the flag.
|
||||
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "737b4d1da8285efa2be0",
|
||||
"artifactVersion": "823524db8f12948bdc73",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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": 1137,
|
||||
"verify-gap": 2192,
|
||||
"audit-partial": 1140,
|
||||
"verify-gap": 2189,
|
||||
"replace-refusal": 40,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
@@ -84606,12 +84606,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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -85085,12 +85087,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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -85218,12 +85222,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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -113088,6 +113088,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",
|
||||
@@ -113979,8 +113981,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"
|
||||
},
|
||||
@@ -114437,8 +114442,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"
|
||||
},
|
||||
@@ -114562,8 +114570,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"
|
||||
},
|
||||
|
||||
@@ -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"]},
|
||||
|
||||
@@ -35,6 +35,8 @@ requested output.
|
||||
|
||||
Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host.
|
||||
|
||||
WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
|
||||
|
||||
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect.
|
||||
|
||||
Unsanitized `speed` program builds, and unsanitized `release` builds of programs small enough for one partition, pass `--import-bitcode <unit.bc>` once per runtime-pack unit the program links that ships bitcode. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted.
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
#include "llvm/ADT/StringExtras.h"
|
||||
#include "llvm/ADT/StringRef.h"
|
||||
#include "llvm/ADT/StringSet.h"
|
||||
#include "llvm/Analysis/RuntimeLibcallInfo.h"
|
||||
#include "llvm/Analysis/TargetLibraryInfo.h"
|
||||
#include "llvm/Bitcode/BitcodeReader.h"
|
||||
#include "llvm/Bitcode/BitcodeWriter.h"
|
||||
#include "llvm/Bitcode/BitcodeWriterPass.h"
|
||||
#include "llvm/CodeGen/MachineDominators.h"
|
||||
#include "llvm/CodeGen/MachineFunction.h"
|
||||
#include "llvm/CodeGen/MachineFunctionPass.h"
|
||||
#include "llvm/CodeGen/MachineModuleInfo.h"
|
||||
#include "llvm/IR/DiagnosticInfo.h"
|
||||
#include "llvm/IR/DiagnosticPrinter.h"
|
||||
#include "llvm/IR/LegacyPassManager.h"
|
||||
@@ -20,6 +26,9 @@
|
||||
#include "llvm/IR/Verifier.h"
|
||||
#include "llvm/IRReader/IRReader.h"
|
||||
#include "llvm/LTO/LTO.h"
|
||||
#include "llvm/Pass.h"
|
||||
#include "llvm/PassInfo.h"
|
||||
#include "llvm/PassRegistry.h"
|
||||
#include "llvm/Passes/PassBuilder.h"
|
||||
#include "llvm/Support/Caching.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
@@ -140,6 +149,138 @@ std::optional<EmitFailure> verify(Module &M, StringRef Code) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Runs WebAssembly's pass that gives multiple-entry loops a single entry only
|
||||
// on functions that have such a loop. The pass first records, for every
|
||||
// block, every block it can reach, so its time and memory grow with the
|
||||
// square of the block count even when it changes nothing: a module
|
||||
// initializer of 40,000 blocks took four minutes and 22 GB in it. It changes
|
||||
// a function only when two loop entries can reach each other, which a
|
||||
// reducible control flow graph never has, so skipping reducible functions
|
||||
// leaves the output unchanged.
|
||||
class IrreducibleControlFlowOnly final : public MachineFunctionPass {
|
||||
public:
|
||||
static char ID;
|
||||
|
||||
explicit IrreducibleControlFlowOnly(FunctionPass *Fix)
|
||||
: MachineFunctionPass(ID), Fix(Fix) {}
|
||||
|
||||
StringRef getPassName() const override { return Fix->getPassName(); }
|
||||
|
||||
bool doInitialization(Module &M) override {
|
||||
MachineFunctionPass::doInitialization(M);
|
||||
return Fix->doInitialization(M);
|
||||
}
|
||||
|
||||
bool doFinalization(Module &M) override { return Fix->doFinalization(M); }
|
||||
|
||||
void getAnalysisUsage(AnalysisUsage &Usage) const override {
|
||||
Fix->getAnalysisUsage(Usage);
|
||||
}
|
||||
|
||||
bool runOnMachineFunction(MachineFunction &MF) override {
|
||||
if (isReducible(MF))
|
||||
return false;
|
||||
// The wrapped pass reads the machine function through the module's
|
||||
// machine code analysis, which this pass requires on its behalf.
|
||||
if (Fix->getResolver() == nullptr) {
|
||||
auto *Resolver = new AnalysisResolver(getResolver()->getPMDataManager());
|
||||
Resolver->addAnalysisImplsPair(
|
||||
&MachineModuleInfoWrapperPass::ID,
|
||||
&getAnalysis<MachineModuleInfoWrapperPass>());
|
||||
Fix->setResolver(Resolver);
|
||||
}
|
||||
return Fix->runOnFunction(MF.getFunction());
|
||||
}
|
||||
|
||||
private:
|
||||
// A graph is reducible when every edge that a depth-first search finds
|
||||
// going back to a block on its stack targets a block that dominates the
|
||||
// edge's source. Blocks the entry cannot reach count as irreducible, since
|
||||
// the wrapped pass also examines them.
|
||||
static bool isReducible(MachineFunction &MF) {
|
||||
MachineDominatorTree Dominators(MF);
|
||||
enum class State : unsigned char { New, Active, Done };
|
||||
std::vector<State> States(MF.getNumBlockIDs(), State::New);
|
||||
SmallVector<std::pair<MachineBasicBlock *,
|
||||
MachineBasicBlock::succ_iterator>,
|
||||
32>
|
||||
Stack;
|
||||
size_t Visited = 1;
|
||||
MachineBasicBlock *Entry = &MF.front();
|
||||
States[Entry->getNumber()] = State::Active;
|
||||
Stack.push_back({Entry, Entry->succ_begin()});
|
||||
while (!Stack.empty()) {
|
||||
auto &[Block, Next] = Stack.back();
|
||||
if (Next == Block->succ_end()) {
|
||||
States[Block->getNumber()] = State::Done;
|
||||
Stack.pop_back();
|
||||
continue;
|
||||
}
|
||||
MachineBasicBlock *Successor = *Next++;
|
||||
State &Seen = States[Successor->getNumber()];
|
||||
if (Seen == State::Active) {
|
||||
if (!Dominators.dominates(Successor, Block))
|
||||
return false;
|
||||
} else if (Seen == State::New) {
|
||||
Seen = State::Active;
|
||||
++Visited;
|
||||
Stack.push_back({Successor, Successor->succ_begin()});
|
||||
}
|
||||
}
|
||||
return Visited == MF.size();
|
||||
}
|
||||
|
||||
std::unique_ptr<FunctionPass> Fix;
|
||||
};
|
||||
|
||||
char IrreducibleControlFlowOnly::ID = 0;
|
||||
|
||||
// The code generation pipeline for one module, with two changes for
|
||||
// WebAssembly. The irreducible control flow pass runs only where it can
|
||||
// change the function (see IrreducibleControlFlowOnly). Object emission also
|
||||
// leaves out the last machine pass, a module pass that records the external
|
||||
// symbols every function uses so that assembly output can declare them
|
||||
// before the first function. A module pass in the middle of the pipeline
|
||||
// keeps the machine code of every function alive until all of them are
|
||||
// compiled. Object files do not contain those declarations, and the printer
|
||||
// creates each symbol when it lowers the first use, so without the pass each
|
||||
// function's machine code is freed once it is printed.
|
||||
class CodeGenPasses final : public legacy::PassManagerBase {
|
||||
public:
|
||||
CodeGenPasses(const TargetMachine &Machine, CodeGenFileType Type)
|
||||
: Wasm(Machine.getTargetTriple().isWasm()),
|
||||
Object(Type == CodeGenFileType::ObjectFile) {}
|
||||
|
||||
void add(Pass *P) override {
|
||||
if (Wasm) {
|
||||
StringRef Argument = passArgument(*P);
|
||||
if (Argument == "wasm-fix-irreducible-control-flow") {
|
||||
Passes.add(
|
||||
new IrreducibleControlFlowOnly(static_cast<FunctionPass *>(P)));
|
||||
return;
|
||||
}
|
||||
if (Object && Argument == "wasm-mclower-prepass") {
|
||||
delete P;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Passes.add(P);
|
||||
}
|
||||
|
||||
void run(Module &M) { Passes.run(M); }
|
||||
|
||||
private:
|
||||
static StringRef passArgument(const Pass &P) {
|
||||
const PassInfo *Info =
|
||||
PassRegistry::getPassRegistry()->getPassInfo(P.getPassID());
|
||||
return Info == nullptr ? StringRef() : Info->getPassArgument();
|
||||
}
|
||||
|
||||
bool Wasm;
|
||||
bool Object;
|
||||
legacy::PassManager Passes;
|
||||
};
|
||||
|
||||
// Writes code for an optimized module to a unique sibling of OutputPath,
|
||||
// which is appended to Temporaries once it exists.
|
||||
std::optional<EmitFailure>
|
||||
@@ -153,7 +294,7 @@ generateCode(Module &M, TargetMachine &Machine, CodeGenFileType Type,
|
||||
return EmitFailure{"output_open_failed", EC.message()};
|
||||
Temporaries.push_back(TemporaryPath);
|
||||
raw_fd_ostream Output(TemporaryFd, true);
|
||||
legacy::PassManager CodeGeneration;
|
||||
CodeGenPasses CodeGeneration(Machine, Type);
|
||||
if (Machine.addPassesToEmitFile(CodeGeneration, Output, nullptr, Type))
|
||||
return EmitFailure{"emission_not_supported",
|
||||
"target does not support the requested file type"};
|
||||
@@ -325,7 +466,7 @@ compileUnoptimizedPartitions(const EmitOptions &Options,
|
||||
P.PB.buildPerModuleDefaultPipeline(OptimizationLevel::O0)
|
||||
.run(**Mod, P.MAM);
|
||||
raw_svector_ostream Output(Objects[I]);
|
||||
legacy::PassManager CodeGeneration;
|
||||
CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
|
||||
if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
|
||||
CodeGenFileType::ObjectFile))
|
||||
return Failures.set({"emission_not_supported",
|
||||
@@ -437,6 +578,44 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
return true;
|
||||
};
|
||||
|
||||
// The backend's own code generation cannot use CodeGenPasses, so
|
||||
// WebAssembly partitions are compiled here with the passes it would add,
|
||||
// and returning false skips it.
|
||||
if (TargetTriple.isWasm())
|
||||
Config.PreCodeGenModuleHook = [&](unsigned Task, const Module &Optimized) {
|
||||
size_t I = Task - 1;
|
||||
if (I >= Count)
|
||||
return true;
|
||||
std::string LookupError;
|
||||
std::unique_ptr<TargetMachine> Machine =
|
||||
createTargetMachine(Options.Target, Options.OptLevel, LookupError);
|
||||
if (!Machine) {
|
||||
Failures.set({"target_machine_failed", LookupError});
|
||||
return false;
|
||||
}
|
||||
// The backend passes the module it owns; only the hook's signature is
|
||||
// const.
|
||||
Module &Mod = const_cast<Module &>(Optimized);
|
||||
Objects[I].clear();
|
||||
raw_svector_ostream Output(Objects[I]);
|
||||
CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
|
||||
TargetLibraryInfoImpl LibraryInfo(Mod.getTargetTriple(),
|
||||
Machine->Options.VecLib);
|
||||
CodeGeneration.add(new TargetLibraryInfoWrapperPass(LibraryInfo));
|
||||
CodeGeneration.add(new RuntimeLibraryInfoWrapper(
|
||||
Mod.getTargetTriple(), Machine->Options.ExceptionModel,
|
||||
Machine->Options.FloatABIType, Machine->Options.EABIVersion,
|
||||
Machine->Options.MCOptions.ABIName, Machine->Options.VecLib));
|
||||
if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
|
||||
CodeGenFileType::ObjectFile)) {
|
||||
Failures.set({"emission_not_supported",
|
||||
"target does not support object emission"});
|
||||
return false;
|
||||
}
|
||||
CodeGeneration.run(Mod);
|
||||
return false;
|
||||
};
|
||||
|
||||
// The link refers to module identifiers for its whole lifetime.
|
||||
std::vector<std::string> Names(Count);
|
||||
for (size_t I = 0; I < Count; ++I)
|
||||
|
||||
@@ -5872,6 +5872,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;
|
||||
@@ -5921,6 +5923,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. */
|
||||
|
||||
@@ -3209,82 +3209,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)!);
|
||||
|
||||
@@ -945,6 +945,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",
|
||||
|
||||
@@ -411,7 +411,7 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
|
||||
SC1120: {
|
||||
feature: "this regex feature",
|
||||
milestone: "later",
|
||||
hint: "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates",
|
||||
hint: "supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates",
|
||||
},
|
||||
// SC1121 (stateful g/y regex tests) shipped — code retired, do not reuse.
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -8894,20 +8894,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
};
|
||||
const pattern = strArg(args[0], "pattern");
|
||||
const flags = strArg(args[1], "flags argument");
|
||||
// Node accepts the 'd' and 'v' flags the engine does not have; a
|
||||
// literal flags string refuses here like the literal regex form.
|
||||
if (flags.kind === "strLit" && args[1] !== undefined) {
|
||||
for (const f of ["d", "v"]) {
|
||||
if (flags.value.includes(f)) {
|
||||
lowerer.unsupported(
|
||||
"SC1120",
|
||||
args[1],
|
||||
f === "d"
|
||||
? "the regex 'd' flag (match indices)"
|
||||
: "the regex 'v' flag (unicode sets)",
|
||||
);
|
||||
}
|
||||
}
|
||||
// Node accepts the 'd' flag the engine does not have; a literal
|
||||
// flags string refuses here like the literal regex form.
|
||||
if (flags.kind === "strLit" && args[1] !== undefined && flags.value.includes("d")) {
|
||||
lowerer.unsupported("SC1120", args[1], "the regex 'd' flag (match indices)");
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
|
||||
@@ -5332,6 +5332,27 @@ export function lowerIntrinsicProperty(
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
// The flag getters answer whether `flags` holds the flag's letter.
|
||||
const flag = REGEX_FLAG_GETTERS.get(name);
|
||||
if (flag !== undefined) {
|
||||
const loc = locOf(expr);
|
||||
const flags: IrExpr = {
|
||||
kind: "regexIntrinsic",
|
||||
method: "flags",
|
||||
receiver: strictReceiver({ kind: "regex" }),
|
||||
args: [],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "strIntrinsic",
|
||||
method: "includes",
|
||||
receiver: flags,
|
||||
args: [{ kind: "strLit", value: flag, type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "test") {
|
||||
lowerer.unsupported("SC1090", expr, "regex methods as values (call 'test' directly)");
|
||||
}
|
||||
@@ -13251,10 +13272,22 @@ function lowerRuntimeKeyIn(
|
||||
return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** RegExp.prototype's flag getters and the flag each one reads. */
|
||||
const REGEX_FLAG_GETTERS: ReadonlyMap<string, string> = new Map([
|
||||
["hasIndices", "d"],
|
||||
["global", "g"],
|
||||
["ignoreCase", "i"],
|
||||
["multiline", "m"],
|
||||
["dotAll", "s"],
|
||||
["unicode", "u"],
|
||||
["unicodeSets", "v"],
|
||||
["sticky", "y"],
|
||||
]);
|
||||
|
||||
/** A regex literal `/ab+c/gi` → regexLit (fresh state per evaluation,
|
||||
* shared bytecode per (pattern, flags) pair). The TS parser has already syntax-checked the literal;
|
||||
* what remains here is the flag-alphabet fence (d and v are
|
||||
* declared-valid TS flags outside this slice). Named capture groups
|
||||
* what remains here is the flag-alphabet fence (d is a
|
||||
* declared-valid TS flag outside this slice). Named capture groups
|
||||
* `(?<name>...)` and `\k<name>` backreferences compile — libregexp
|
||||
* executes them natively, replace templates resolve `$<name>` at
|
||||
* runtime, and `.groups` reads desugar at their access sites
|
||||
@@ -13265,20 +13298,19 @@ export function lowerRegexLiteral(lowerer: Lowerer, expr: ts.RegularExpressionLi
|
||||
const text = expr.text;
|
||||
const lastSlash = text.lastIndexOf("/");
|
||||
const pattern = text.slice(1, lastSlash);
|
||||
const flags = text.slice(lastSlash + 1);
|
||||
for (const f of flags) {
|
||||
if (!"gimsuy".includes(f)) {
|
||||
const written = text.slice(lastSlash + 1);
|
||||
for (const f of written) {
|
||||
if (!"gimsuvy".includes(f)) {
|
||||
lowerer.unsupported(
|
||||
"SC1120",
|
||||
expr,
|
||||
f === "d"
|
||||
? "the regex 'd' flag (match indices)"
|
||||
: f === "v"
|
||||
? "the regex 'v' flag (unicode sets)"
|
||||
: `the regex '${f}' flag`,
|
||||
f === "d" ? "the regex 'd' flag (match indices)" : `the regex '${f}' flag`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// `flags`, `toString()` and inspection list the flags in getter order,
|
||||
// whatever order the literal spells them in.
|
||||
const flags = [..."gimsuvy"].filter((f) => written.includes(f)).join("");
|
||||
return { kind: "regexLit", pattern, flags, type: REGEX, loc: locOf(expr) };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -2564,7 +2564,7 @@ export type IrExpr =
|
||||
}
|
||||
/** A regex literal `/ab+c/gi`. `pattern` is the text between the slashes
|
||||
* exactly as written (escapes UNprocessed — the regex engine parses them),
|
||||
* `flags` the trailing flags in source order (alphabet fenced to gimsuy by
|
||||
* `flags` the trailing flags in getter order (alphabet fenced to gimsuvy by
|
||||
* the frontend). Backends intern ONE immortal static per (pattern, flags)
|
||||
* pair — like string literals, so repeated evaluation is free and
|
||||
* `re === re` would hold — and compile the pattern lazily at first use
|
||||
|
||||
@@ -1932,8 +1932,8 @@ function validateFunction(
|
||||
}
|
||||
case "regexLit": {
|
||||
if (e.type.kind !== "regex") err("regexLit must be regex-typed", e.loc);
|
||||
if (!/^[gimsuy]*$/.test(e.flags)) {
|
||||
err(`regexLit flags "${e.flags}" outside the supported alphabet (gimsuy)`, e.loc);
|
||||
if (!/^[gimsuvy]*$/.test(e.flags)) {
|
||||
err(`regexLit flags "${e.flags}" outside the supported alphabet (gimsuvy)`, e.loc);
|
||||
}
|
||||
if (new Set(e.flags).size !== e.flags.length) {
|
||||
err(`regexLit flags "${e.flags}" contain a duplicate`, e.loc);
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
"name": "this regex feature",
|
||||
"status": "unsupported",
|
||||
"code": "SC1120",
|
||||
"note": "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates"
|
||||
"note": "supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates"
|
||||
},
|
||||
{
|
||||
"id": "diagnostic.sc2001",
|
||||
|
||||
@@ -205,6 +205,72 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
|
||||
});
|
||||
});
|
||||
|
||||
test("WebAssembly emission gives irreducible loops a single entry", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-helper-irreducible-"));
|
||||
dirs.push(dir);
|
||||
const input = join(dir, "loops.ll");
|
||||
// @irreducible enters its loop at %a or %b; @reducible has one loop entry.
|
||||
await writeFile(
|
||||
input,
|
||||
`declare void @step(i32)
|
||||
|
||||
define i32 @irreducible(i32 %n, i1 %c) {
|
||||
entry:
|
||||
br i1 %c, label %a, label %b
|
||||
a:
|
||||
%ia = phi i32 [ 0, %entry ], [ %ib1, %b ]
|
||||
call void @step(i32 %ia)
|
||||
%ia1 = add i32 %ia, 1
|
||||
%da = icmp slt i32 %ia1, %n
|
||||
br i1 %da, label %b, label %done
|
||||
b:
|
||||
%ib = phi i32 [ 0, %entry ], [ %ia1, %a ]
|
||||
call void @step(i32 %ib)
|
||||
%ib1 = add i32 %ib, 2
|
||||
%db = icmp slt i32 %ib1, %n
|
||||
br i1 %db, label %a, label %done
|
||||
done:
|
||||
%r = phi i32 [ %ia1, %a ], [ %ib1, %b ]
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i32 @reducible(i32 %n) {
|
||||
entry:
|
||||
br label %loop
|
||||
loop:
|
||||
%i = phi i32 [ 0, %entry ], [ %i1, %loop ]
|
||||
call void @step(i32 %i)
|
||||
%i1 = add i32 %i, 1
|
||||
%d = icmp slt i32 %i1, %n
|
||||
br i1 %d, label %loop, label %done
|
||||
done:
|
||||
ret i32 %i1
|
||||
}
|
||||
`,
|
||||
);
|
||||
const wasm = "wasm32-unknown-wasi";
|
||||
const assemblyPath = join(dir, "loops.s");
|
||||
const assemblyArgs = helperArgs(input, assemblyPath, wasm);
|
||||
assemblyArgs[assemblyArgs.indexOf("--filetype") + 1] = "asm";
|
||||
expect((await run(helper, assemblyArgs)).exitCode).toBe(0);
|
||||
const assembly = await readFile(assemblyPath, "utf8");
|
||||
const body = (name: string) =>
|
||||
assembly.slice(assembly.indexOf(`\n${name}:\n`)).split("end_function")[0]!;
|
||||
// The dispatch block that replaces the second loop entry.
|
||||
expect(body("irreducible")).toMatch(/\bbr_table\b/);
|
||||
expect(body("reducible")).not.toMatch(/\bbr_table\b/);
|
||||
|
||||
const unoptimized = helperArgs(input, join(dir, "dev.o"), wasm);
|
||||
unoptimized[unoptimized.indexOf("--opt-level") + 1] = "0";
|
||||
expect((await run(helper, unoptimized)).exitCode).toBe(0);
|
||||
const partitioned = [
|
||||
...helperArgs(input, join(dir, "a.o"), wasm),
|
||||
"--output",
|
||||
join(dir, "b.o"),
|
||||
];
|
||||
expect((await run(helper, partitioned)).exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("LLVM fatal diagnostics remain process-isolated JSON", async () => {
|
||||
const result = await execFileAsync(helper, ["version", "--format=json"], {
|
||||
encoding: "utf8",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5857-regexp-unicode-sets.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5915-dyn-error-string-conversions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5990-worker-uncloneable.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5991-worker-async-dispose.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-discarded-heaps.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -630,13 +630,6 @@ 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);
|
||||
#endif
|
||||
|
||||
/* Moves every block other threads freed into this arena's free lists. */
|
||||
@@ -645,13 +638,6 @@ 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;
|
||||
@@ -686,7 +672,6 @@ 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;
|
||||
}
|
||||
@@ -899,303 +884,6 @@ 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, the trim helper 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]; /* handed to the helper */
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
#endif
|
||||
}
|
||||
#define SCR_SA_WORKER_STARTING() scr_sa_worker_starting()
|
||||
#endif
|
||||
|
||||
#ifdef SCR_SA_TRIM
|
||||
@@ -1218,36 +906,12 @@ static void *scr_sa_trim_main(void *arg) {
|
||||
unsigned i = (unsigned)(uintptr_t)arg;
|
||||
scr_sa_arena = (int)i;
|
||||
scr_sa = scr_sa_parked[i];
|
||||
ScrSaKeep *k = scr_sa_parked_keep[i];
|
||||
scr_sa_parked_keep[i] = NULL;
|
||||
if (k) scr_sa_discard(k);
|
||||
else scr_sa_trim();
|
||||
scr_sa_trim();
|
||||
scr_sa_release(i);
|
||||
atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_release);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Hands the calling thread's arena to a trim helper (with `k`, to discard
|
||||
* the worker's memory); false if no helper could start. */
|
||||
static bool scr_sa_start_helper(unsigned i, ScrSaKeep *k) {
|
||||
scr_sa_parked[i] = scr_sa;
|
||||
scr_sa_parked_keep[i] = k;
|
||||
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) {
|
||||
scr_sa_parked_keep[i] = NULL;
|
||||
atomic_fetch_sub_explicit(&scr_sa_trimming, 1, memory_order_relaxed);
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
void scr_sa_wait_trims(void) {
|
||||
while (atomic_load_explicit(&scr_sa_trimming, memory_order_acquire)) {
|
||||
struct timespec pause = {0, 1000000};
|
||||
@@ -1256,7 +920,6 @@ void scr_sa_wait_trims(void) {
|
||||
}
|
||||
#else
|
||||
void scr_sa_wait_trims(void) {}
|
||||
void scr_sa_thread_shares(void) {}
|
||||
#endif
|
||||
|
||||
void scr_sa_thread_park(void) {
|
||||
@@ -1266,25 +929,18 @@ void scr_sa_thread_park(void) {
|
||||
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, and later frees on this thread are dropped. */
|
||||
scr_sa_discarded = true;
|
||||
if (!scr_sa_start_helper(i, k)) {
|
||||
scr_sa_discard(k);
|
||||
scr_sa_release(i);
|
||||
return;
|
||||
}
|
||||
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()) {
|
||||
if (scr_sa_start_helper(i, NULL)) {
|
||||
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) {
|
||||
/* 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);
|
||||
@@ -1292,6 +948,7 @@ 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
|
||||
@@ -1314,9 +971,6 @@ 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) {
|
||||
@@ -1328,9 +982,6 @@ 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
|
||||
@@ -1369,15 +1020,6 @@ __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. */
|
||||
@@ -1409,9 +1051,6 @@ __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;
|
||||
}
|
||||
|
||||
@@ -1426,14 +1065,8 @@ 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). */
|
||||
@@ -1449,15 +1082,10 @@ 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
|
||||
@@ -1488,10 +1116,7 @@ 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;
|
||||
SCR_SA_WORKER_STARTING();
|
||||
}
|
||||
void scr_sa_thread_starting(void) { scr_sa_thread_large = 1; }
|
||||
|
||||
static void scr_sa_thread_reserve(void) {
|
||||
void *blocks[SCR_SA_THREAD_HEAP_RESERVE / SCR_SA_THREAD_HEAP_STEP + 1];
|
||||
@@ -1511,7 +1136,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) { SCR_SA_WORKER_STARTING(); }
|
||||
void scr_sa_thread_starting(void) {}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
@@ -429,273 +429,6 @@ 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 */
|
||||
/* 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();
|
||||
assert(scr_sa.base == discard_base);
|
||||
/* None of the arena's older blocks or free blocks is reused. */
|
||||
for (unsigned i = 0; i < DISCARD_OLD; i++) assert(discard_lo > (uintptr_t)old_blocks[i]);
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
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 */
|
||||
@@ -713,7 +446,6 @@ int main(void) {
|
||||
check_threads();
|
||||
#ifdef __linux__
|
||||
check_trim();
|
||||
check_discard();
|
||||
#endif
|
||||
printf("allocator checks passed: size-class workers\n");
|
||||
#else
|
||||
|
||||
@@ -669,6 +669,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 +894,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 +961,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);
|
||||
|
||||
@@ -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]");
|
||||
|
||||
@@ -428,13 +428,11 @@ 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);
|
||||
@@ -590,7 +588,6 @@ 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,18 +8,13 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 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. */
|
||||
/* The table unit needs only scr_trap and scr_throw_error_msg from the
|
||||
* rest of the runtime; refusals 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);
|
||||
@@ -227,7 +222,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -5947,11 +5967,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 +6004,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 +6079,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 +6474,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 +6596,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);
|
||||
@@ -9489,6 +9581,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 +9646,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,7 +10709,6 @@ 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;
|
||||
|
||||
@@ -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,7 +278,6 @@ 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;
|
||||
@@ -292,7 +295,6 @@ 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;
|
||||
@@ -556,7 +558,6 @@ 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;
|
||||
|
||||
@@ -341,18 +341,11 @@ 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, and lets the thread's memory go back when it exits
|
||||
* (scr_alloc.c). */
|
||||
* allocator heap (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
|
||||
|
||||
@@ -85,6 +85,7 @@ static int scr_lre_flags(const ScrStr *flags) {
|
||||
case 's': mask |= LRE_FLAG_DOTALL; break;
|
||||
case 'u': mask |= LRE_FLAG_UNICODE; break;
|
||||
case 'y': mask |= LRE_FLAG_STICKY; break;
|
||||
case 'v': mask |= LRE_FLAG_UNICODE_SETS; break;
|
||||
default:
|
||||
scr_trap_fmt("scriptc: internal error: unexpected regex flag '%c'\n",
|
||||
flags->data[i]);
|
||||
@@ -201,7 +202,7 @@ static uint8_t *scr_regex_bc(ScrRegex *re) {
|
||||
const char *pat = re->source->data;
|
||||
size_t pat_len = re->source->len;
|
||||
char *cesu = NULL;
|
||||
if (!(flags & LRE_FLAG_UNICODE)) {
|
||||
if (!(flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS))) {
|
||||
cesu = scr_pattern_cesu8(re->source, &pat_len);
|
||||
if (cesu) pat = cesu;
|
||||
}
|
||||
@@ -2042,11 +2043,9 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) {
|
||||
}
|
||||
/* Valid in Node, absent from the engine: name the gap rather than
|
||||
* report a flag Node accepts as invalid. */
|
||||
if (seen_flags & ((1u << 6) | (1u << 7))) {
|
||||
if (seen_flags & (1u << 6)) {
|
||||
static const char d_msg[] = "the regex 'd' flag (match indices) is not supported yet [SC1120]";
|
||||
static const char v_msg[] = "the regex 'v' flag (unicode sets) is not supported yet [SC1120]";
|
||||
if (seen_flags & (1u << 6)) scr_throw_error_msg(SCR_ERR_ERROR, d_msg, sizeof d_msg - 1);
|
||||
else scr_throw_error_msg(SCR_ERR_ERROR, v_msg, sizeof v_msg - 1);
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, d_msg, sizeof d_msg - 1);
|
||||
return NULL;
|
||||
}
|
||||
ScrRegex *re = calloc(1, sizeof *re);
|
||||
@@ -2057,11 +2056,11 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) {
|
||||
re->source = pattern->len > 0 ? scr_str_retain(pattern) : scr_str_new("(?:)", 4);
|
||||
/* Public flags use getter order, independently of constructor spelling.
|
||||
* Validate first so duplicates and invalid flags retain their errors. */
|
||||
char canonical[6];
|
||||
char canonical[8];
|
||||
size_t flag_count = 0;
|
||||
const char *order = "gimsuy";
|
||||
for (unsigned i = 0; i < 6; i++) {
|
||||
if (seen_flags & (1u << i)) canonical[flag_count++] = order[i];
|
||||
static const unsigned order_bits[] = {0, 1, 2, 3, 4, 7, 5};
|
||||
for (unsigned i = 0; i < sizeof order_bits / sizeof order_bits[0]; i++) {
|
||||
if (seen_flags & (1u << order_bits[i])) canonical[flag_count++] = "gimsuvy"[i];
|
||||
}
|
||||
re->flags = flag_count == flags->len && memcmp(canonical, flags->data, flag_count) == 0
|
||||
? scr_str_retain(flags) : scr_str_new(canonical, flag_count);
|
||||
@@ -2078,7 +2077,7 @@ ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags) {
|
||||
const char *pat = re->source->data;
|
||||
size_t pat_len = re->source->len;
|
||||
char *cesu = NULL;
|
||||
if (!(lre_flags & LRE_FLAG_UNICODE)) {
|
||||
if (!(lre_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS))) {
|
||||
cesu = scr_pattern_cesu8(re->source, &pat_len);
|
||||
if (cesu) pat = cesu;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
+3
-3
@@ -1020,7 +1020,7 @@ static int parse_class_string_disjunction(REParseState *s, REStringList *cr,
|
||||
for(;;) {
|
||||
str.size = 0;
|
||||
while (*p != '}' && *p != '|') {
|
||||
c = get_class_atom(s, NULL, &p, false);
|
||||
c = get_class_atom(s, NULL, &p, true);
|
||||
if (c < 0)
|
||||
goto fail;
|
||||
if (dbuf_put_u32(&str, c)) {
|
||||
@@ -1193,7 +1193,7 @@ static int get_class_atom(REParseState *s, REStringList *cr,
|
||||
case '^':
|
||||
case '`':
|
||||
case '~':
|
||||
if (s->unicode_sets && p[1] == c) {
|
||||
if (s->unicode_sets && inclass && p[1] == c) {
|
||||
/* forbidden double characters */
|
||||
return re_parse_error(s, "invalid class set operation in regular expression");
|
||||
}
|
||||
@@ -1208,7 +1208,7 @@ static int get_class_atom(REParseState *s, REStringList *cr,
|
||||
case '/':
|
||||
case '-':
|
||||
case '|':
|
||||
if (s->unicode_sets) {
|
||||
if (s->unicode_sets && inclass) {
|
||||
/* invalid characters in unicode sets */
|
||||
return re_parse_error(s, "invalid character in class in regular expression");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// The regex v flag (unicode sets): class subtraction and intersection, nested
|
||||
// classes, \q{...} string literals, properties of strings, case folding,
|
||||
// code-point iteration, and the constructor with literal and computed flags.
|
||||
// Regex literals report their flags in getter order. Node.js prints the same.
|
||||
const letters = /[\p{L}--[a-z]]/gv;
|
||||
console.log(letters.flags, letters.source, String(letters), letters);
|
||||
console.log("aBcD-É".match(letters));
|
||||
console.log(/[[a-z]&&[aeiou]]/v.test("e"), /[[a-z]&&[aeiou]]/v.test("b"));
|
||||
console.log("vowels: education".replace(/[[a-z]&&[^aeiou]]/gv, "_"));
|
||||
console.log("x😀y".match(/[\q{😀|xy}]/gv), "xy😀".match(/[\q{😀|xy}]/gv));
|
||||
console.log("abcab".match(/[\q{abc}a]/gv));
|
||||
console.log(/^\p{RGI_Emoji}$/v.test("👍🏽"), /^\p{RGI_Emoji}$/v.test("a"));
|
||||
console.log("hi 🇫🇷 1️⃣".match(/\p{RGI_Emoji_Flag_Sequence}|\p{Emoji_Keycap_Sequence}/gv));
|
||||
console.log("family \u{1F468}\u200D\u{1F469}\u200D\u{1F467} ok".replace(/\p{RGI_Emoji_ZWJ_Sequence}/v, "[zwj]"));
|
||||
console.log(/[^\d--[5]]/v.test("5"), /[^\d--[5]]/v.test("7"), /[^\d--[5]]/v.test("x"));
|
||||
console.log(/[[0-9]--[5-9]]+/v.exec("98123")?.[0]);
|
||||
console.log(/[\p{Lu}--[A-C]]/vi.test("a"), /[\p{Lu}--[A-C]]/vi.test("d"), /[^\p{Lu}]/vi.test("A"));
|
||||
console.log("😀😀".replace(/(?:)/gv, "-"), "😀😀".split(/(?:)/v));
|
||||
console.log("a😀b".replace(/./gv, (c) => `<${c}>`));
|
||||
console.log("A1b2C3".replace(/[\w--\d]/gv, (c) => c.toLowerCase()));
|
||||
console.log("one, two;three".split(/[[\p{P}\s]--[\-]]+/v));
|
||||
const m = /(?<word>[\p{L}--\p{Lu}]+)-(?<num>[\d--[0]]+)/v.exec("ABcd-102");
|
||||
if (m) console.log(m[0], m.index, m.groups?.word, m.groups?.num);
|
||||
console.log("2024-07".replace(/(?<y>[\d]{4})-(?<mo>\d\d)/v, "$<mo>/$<y>"));
|
||||
|
||||
const spelled = /x/vgimsy;
|
||||
console.log(spelled.flags, String(spelled), /x/yg.flags, /x/mi.flags);
|
||||
console.log(letters.global, letters.unicodeSets, letters.unicode, letters.ignoreCase, letters.hasIndices);
|
||||
console.log(spelled.multiline, spelled.dotAll, spelled.sticky, /x/u.unicode, /x/u.unicodeSets);
|
||||
|
||||
console.log(new RegExp("[\\w--\\d]", "v").test("5"), new RegExp("[\\w--\\d]", "vg").flags);
|
||||
const computed = ["v", "gv", "iv"];
|
||||
for (const flags of computed) {
|
||||
const re = new RegExp("[[0-9a-f]--[5]]", flags);
|
||||
console.log(flags, re.flags, re.test("5"), re.test("F"));
|
||||
}
|
||||
for (const flags of ["uv", "vv"]) {
|
||||
try {
|
||||
new RegExp("a", flags);
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
const u: unknown = /[\q{abc}]/v;
|
||||
console.log(u instanceof RegExp, String(u));
|
||||
// Punctuation that only class contents reserve stays literal outside them.
|
||||
console.log(/x-y/v.test("x-y"), /a,,b/v.test("a,,b"), /a&&b|!!/v.exec("!!")?.[0], /a--b/v.test("a--b"));
|
||||
console.log("a-b".match(/[\q{\-}]/gv), "key-value".split(/-/v));
|
||||
@@ -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,59 @@
|
||||
// markAsUncloneable: structuredClone and postMessage throw DataCloneError
|
||||
// for a marked object, error or class instance, also nested in an object;
|
||||
// the mark has no effect on primitives, arrays and typed arrays.
|
||||
import { markAsUncloneable, MessageChannel, isInternalThread } from "node:worker_threads";
|
||||
|
||||
console.log("isInternalThread", isInternalThread);
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
port2.on("message", (m) => console.log("received", JSON.stringify(m)));
|
||||
|
||||
class Point {
|
||||
constructor(x) {
|
||||
this.x = x;
|
||||
}
|
||||
}
|
||||
const cases = {
|
||||
object: { a: 1 },
|
||||
error: new Error("e"),
|
||||
instance: new Point(1),
|
||||
array: [1, 2],
|
||||
bytes: new Uint8Array(2),
|
||||
};
|
||||
for (const [name, value] of Object.entries(cases)) {
|
||||
markAsUncloneable(value);
|
||||
let cloned;
|
||||
let posted;
|
||||
try {
|
||||
structuredClone(value);
|
||||
cloned = "cloned";
|
||||
} catch (e) {
|
||||
cloned = `${e.name} ${e.message} ${e.code}`;
|
||||
}
|
||||
try {
|
||||
port1.postMessage(value);
|
||||
posted = "posted";
|
||||
} catch (e) {
|
||||
posted = `${e.name} ${e.message}`;
|
||||
}
|
||||
console.log(name, "|", cloned, "|", posted);
|
||||
}
|
||||
markAsUncloneable(5);
|
||||
markAsUncloneable("text");
|
||||
markAsUncloneable(undefined);
|
||||
console.log("primitives", structuredClone(5), structuredClone("text"));
|
||||
const marked = { inner: true };
|
||||
markAsUncloneable(marked);
|
||||
markAsUncloneable(marked);
|
||||
try {
|
||||
structuredClone({ nested: marked });
|
||||
} catch (e) {
|
||||
console.log("nested", e.name, e.message);
|
||||
}
|
||||
try {
|
||||
port1.postMessage({ nested: marked });
|
||||
} catch (e) {
|
||||
console.log("nested post", e.name, e.message);
|
||||
}
|
||||
const copy = { ...marked };
|
||||
console.log("spread copy", JSON.stringify(structuredClone(copy)));
|
||||
setTimeout(() => port1.close(), 50);
|
||||
@@ -0,0 +1,25 @@
|
||||
// worker[Symbol.asyncDispose]() terminates the worker and resolves with
|
||||
// undefined after its 'exit' event; `await using` disposes a Worker at the
|
||||
// end of its block.
|
||||
import { Worker, isMainThread, parentPort } from "node:worker_threads";
|
||||
|
||||
if (isMainThread) {
|
||||
const worker = new Worker(new URL(import.meta.url));
|
||||
const dispose = worker[Symbol.asyncDispose];
|
||||
console.log(typeof dispose, dispose.name, dispose.length);
|
||||
worker.on("exit", (code) => console.log("exit", code));
|
||||
await new Promise((resolve) => worker.once("message", resolve));
|
||||
const result = await worker[Symbol.asyncDispose]();
|
||||
console.log("disposed", result);
|
||||
console.log("again", await worker[Symbol.asyncDispose]());
|
||||
{
|
||||
await using scoped = new Worker(new URL(import.meta.url));
|
||||
scoped.on("exit", (code) => console.log("scoped exit", code));
|
||||
await new Promise((resolve) => scoped.once("message", resolve));
|
||||
console.log("leaving block");
|
||||
}
|
||||
console.log("after block");
|
||||
} else {
|
||||
parentPort.postMessage("ready");
|
||||
setInterval(() => {}, 1000);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
import { publish } from "@scriptc/threads";
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
|
||||
// Workers whose memory goes back when they exit, and workers whose memory
|
||||
// stays because other threads hold what they allocated. Builder workers
|
||||
// create ProgramLifetime instances (which scriptc never frees), ordinary
|
||||
// objects, strings and large typed arrays, post a checksum and exit. A
|
||||
// publisher worker posts published graphs (a class graph and a plain
|
||||
// object) and exits. The main thread reads those graphs again after each
|
||||
// wave of builders, which in scriptc on Linux reuse the memory of the
|
||||
// builders before them.
|
||||
|
||||
class Node implements ProgramLifetime {
|
||||
readonly kind: number;
|
||||
readonly label: string;
|
||||
readonly children: Node[] = [];
|
||||
parent: Node | undefined = undefined;
|
||||
constructor(kind: number, label: string) {
|
||||
this.kind = kind;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
type Summary = { readonly name: string; readonly values: readonly number[]; readonly nested: { readonly total: number } };
|
||||
|
||||
type Job = { readonly role: string; readonly seed: number };
|
||||
|
||||
function build(seed: number, count: number): Node {
|
||||
const root = new Node(seed, "root" + seed);
|
||||
let parent = root;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const node = new Node((i * 7 + seed) % 31, "n" + (i % 101));
|
||||
node.parent = parent;
|
||||
parent.children.push(node);
|
||||
if (i % 64 === 63) parent = node;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function checksum(root: Node): number {
|
||||
let sum = 0;
|
||||
const stack: Node[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
sum = (sum * 31 + node.kind + node.label.length + node.children.length) % 1_000_003;
|
||||
for (const child of node.children) stack.push(child);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
function startWorker(job: Job, onMessage: (value: unknown) => void): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: job });
|
||||
worker.on("message", onMessage);
|
||||
worker.once("messageerror", reject);
|
||||
worker.once("error", reject);
|
||||
worker.once("exit", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
let tree: Node | undefined = undefined;
|
||||
let summary: Summary | undefined = undefined;
|
||||
// The publisher posts the tree, then the summary.
|
||||
await startWorker({ role: "publisher", seed: 3 }, (value) => {
|
||||
if (tree === undefined) tree = value as Node;
|
||||
else summary = value as Summary;
|
||||
});
|
||||
const describe = (): string =>
|
||||
`tree ${checksum(tree!)} summary ${summary!.name} ${summary!.values.join(",")} ${summary!.nested.total}`;
|
||||
console.log("published", describe());
|
||||
for (let wave = 0; wave < 3; wave++) {
|
||||
const sums: number[] = [];
|
||||
await Promise.all(
|
||||
[1, 2, 3, 4].map((k) => startWorker({ role: "builder", seed: wave * 10 + k }, (value) => sums.push(value as number))),
|
||||
);
|
||||
sums.sort((a, b) => a - b);
|
||||
console.log("wave", wave, sums.join(" "), describe());
|
||||
}
|
||||
} else {
|
||||
const job = workerData as Job;
|
||||
if (job.role === "publisher") {
|
||||
parentPort!.postMessage(publish(build(job.seed, 20_000)));
|
||||
const values: number[] = [];
|
||||
for (let i = 0; i < 50; i++) values.push(i * job.seed);
|
||||
parentPort!.postMessage(publish({ name: "summary" + job.seed, values, nested: { total: values.length } }));
|
||||
} else {
|
||||
const root = build(job.seed, 150_000);
|
||||
const samples: Float64Array[] = [];
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const sample = new Float64Array(8192);
|
||||
for (let j = 0; j < sample.length; j += 97) sample[j] = i + j + job.seed;
|
||||
samples.push(sample);
|
||||
}
|
||||
let extra = 0;
|
||||
for (const sample of samples) extra = (extra + sample[97]! + sample[8148]!) % 1_000_003;
|
||||
const words: string[] = [];
|
||||
for (let i = 0; i < 20_000; i++) words.push(`word-${i % 977}-${job.seed}`);
|
||||
parentPort!.postMessage((checksum(root) + extra + words.join("").length) % 1_000_003);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// new RegExp with literal d or v flags refuses like the regex literal
|
||||
// forms; computed flags reach the runtime, which names the same gap.
|
||||
const sets = new RegExp("[\\w--\\d]", "v");
|
||||
// new RegExp with a literal d flag refuses like the regex literal form;
|
||||
// computed flags reach the runtime, which names the same gap.
|
||||
const indices = new RegExp("cat", "dg");
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// The d/v flags are outside the native slice; .groups needs a
|
||||
// The d flag is outside the native slice; .groups needs a
|
||||
// statically-known regex. Named capture groups compile (corpus 2604).
|
||||
// Method values and regex union arms remain unsupported; arrays compile.
|
||||
const indices = /cat/d;
|
||||
const sets = /[\p{L}]/v;
|
||||
const asValue = /x/.test;
|
||||
const maybe: RegExp | undefined = /a/;
|
||||
function readGroups(re: RegExp): void {
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
regex-constructor-flags.ts:3:39 - error SC1120: the regex 'v' flag (unicode sets) is not supported yet
|
||||
regex-constructor-flags.ts:3:35 - error SC1120: the regex 'd' flag (match indices) is not supported yet
|
||||
|
||||
2 | // forms; computed flags reach the runtime, which names the same gap.
|
||||
3 | const sets = new RegExp("[\\w--\\d]", "v");
|
||||
| ^~~
|
||||
4 | const indices = new RegExp("cat", "dg");
|
||||
|
||||
hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates
|
||||
|
||||
regex-constructor-flags.ts:4:35 - error SC1120: the regex 'd' flag (match indices) is not supported yet
|
||||
|
||||
3 | const sets = new RegExp("[\\w--\\d]", "v");
|
||||
4 | const indices = new RegExp("cat", "dg");
|
||||
2 | // computed flags reach the runtime, which names the same gap.
|
||||
3 | const indices = new RegExp("cat", "dg");
|
||||
| ^~~~
|
||||
5 |
|
||||
4 |
|
||||
|
||||
hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates
|
||||
hint: supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates
|
||||
@@ -3,31 +3,22 @@ regex.ts:4:17 - error SC1120: the regex 'd' flag (match indices) is not supporte
|
||||
3 | // Method values and regex union arms remain unsupported; arrays compile.
|
||||
4 | const indices = /cat/d;
|
||||
| ^~~~~~
|
||||
5 | const sets = /[\p{L}]/v;
|
||||
5 | const asValue = /x/.test;
|
||||
|
||||
hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates
|
||||
hint: supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates
|
||||
|
||||
regex.ts:5:14 - error SC1120: the regex 'v' flag (unicode sets) is not supported yet
|
||||
regex.ts:5:17 - error SC1090: regex methods as values (call 'test' directly) are not supported yet
|
||||
|
||||
4 | const indices = /cat/d;
|
||||
5 | const sets = /[\p{L}]/v;
|
||||
| ^~~~~~~~~~
|
||||
6 | const asValue = /x/.test;
|
||||
|
||||
hint: supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates
|
||||
|
||||
regex.ts:6:17 - error SC1090: regex methods as values (call 'test' directly) are not supported yet
|
||||
|
||||
5 | const sets = /[\p{L}]/v;
|
||||
6 | const asValue = /x/.test;
|
||||
5 | const asValue = /x/.test;
|
||||
| ^~~~~~~~
|
||||
7 | const maybe: RegExp | undefined = /a/;
|
||||
6 | const maybe: RegExp | undefined = /a/;
|
||||
|
||||
regex.ts:10:22 - error SC2020: 'RegExpExecArray.groups' is part of the standard library types but has no scriptc lowering yet
|
||||
regex.ts:9:22 - error SC2020: 'RegExpExecArray.groups' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
9 | const m = re.exec("2024-07");
|
||||
10 | if (m) console.log(m.groups);
|
||||
8 | const m = re.exec("2024-07");
|
||||
9 | if (m) console.log(m.groups);
|
||||
| ^~~~~~~~
|
||||
11 | }
|
||||
10 | }
|
||||
|
||||
hint: '.groups' lowers when the match's regex is STATICALLY known — a regex literal, or a const initialized with one (the group-name table is built at compile time) — and the read is not an optional-chain step: narrow the match instead (if (m) { m.groups } or m!.groups)
|
||||
@@ -1866,6 +1866,7 @@
|
||||
"5838-qs-stringify-bigint.ts": 2207,
|
||||
"5842-qs-parse-null-prototype.ts": 1774,
|
||||
"5843-qs-parse-null-prototype-js.mjs": 1559,
|
||||
"5857-regexp-unicode-sets.ts": 3350,
|
||||
"5858-disposable-stack.ts": 3050,
|
||||
"5859-disposable-stack-js.js": 3100,
|
||||
"5870-worker-thread-names.ts": 3106,
|
||||
@@ -1890,6 +1891,7 @@
|
||||
"5912-dyn-class-conversion-methods.ts": 3251,
|
||||
"5913-prototype-type-error-messages.ts": 2484,
|
||||
"5914-dyn-in-class-prototype-members.ts": 3169,
|
||||
"5915-dyn-error-string-conversions.ts": 3200,
|
||||
"5922-matchall-groups-callbacks.ts": 3650,
|
||||
"5923-matchall-groups-js.js": 3970,
|
||||
"5939-js-let-object-reassigned-wider.js": 1945,
|
||||
@@ -1904,6 +1906,8 @@
|
||||
"5970-net-connect-host-name.ts": 2400,
|
||||
"5971-buffer-chunks-to-string.ts": 2700,
|
||||
"5972-net-set-encoding-listeners.ts": 2550,
|
||||
"5990-worker-uncloneable.mjs": 3283,
|
||||
"5991-worker-async-dispose.mjs": 5699,
|
||||
"5992-worker-thread-name-lifetime.mjs": 2687,
|
||||
"600-closures-basic.ts": 2349,
|
||||
"601-closures-loops.ts": 1788,
|
||||
@@ -2417,7 +2421,6 @@
|
||||
"native-worker-clone-errors.ts": 2347,
|
||||
"native-worker-commonjs.cjs": 2188,
|
||||
"native-worker-delayed-messages.ts": 3114,
|
||||
"native-worker-discarded-heaps.ts": 4430,
|
||||
"native-worker-environment.ts": 3538,
|
||||
"native-worker-error.ts": 2810,
|
||||
"native-worker-exception-state.ts": 3938,
|
||||
@@ -4555,6 +4558,7 @@
|
||||
"5838-qs-stringify-bigint.ts": 3519,
|
||||
"5842-qs-parse-null-prototype.ts": 7077,
|
||||
"5843-qs-parse-null-prototype-js.mjs": 5395,
|
||||
"5857-regexp-unicode-sets.ts": 3900,
|
||||
"5858-disposable-stack.ts": 3750,
|
||||
"5859-disposable-stack-js.js": 3600,
|
||||
"5870-worker-thread-names.ts": 31697,
|
||||
@@ -4579,6 +4583,7 @@
|
||||
"5912-dyn-class-conversion-methods.ts": 4424,
|
||||
"5913-prototype-type-error-messages.ts": 4068,
|
||||
"5914-dyn-in-class-prototype-members.ts": 4327,
|
||||
"5915-dyn-error-string-conversions.ts": 4247,
|
||||
"5922-matchall-groups-callbacks.ts": 5960,
|
||||
"5923-matchall-groups-js.js": 6650,
|
||||
"5939-js-let-object-reassigned-wider.js": 4436,
|
||||
@@ -4593,6 +4598,8 @@
|
||||
"5970-net-connect-host-name.ts": 2800,
|
||||
"5971-buffer-chunks-to-string.ts": 3100,
|
||||
"5972-net-set-encoding-listeners.ts": 3000,
|
||||
"5990-worker-uncloneable.mjs": 22432,
|
||||
"5991-worker-async-dispose.mjs": 25768,
|
||||
"5992-worker-thread-name-lifetime.mjs": 28932,
|
||||
"600-closures-basic.ts": 2534,
|
||||
"601-closures-loops.ts": 2297,
|
||||
@@ -5106,7 +5113,6 @@
|
||||
"native-worker-clone-errors.ts": 3629,
|
||||
"native-worker-commonjs.cjs": 3085,
|
||||
"native-worker-delayed-messages.ts": 28752,
|
||||
"native-worker-discarded-heaps.ts": 39440,
|
||||
"native-worker-environment.ts": 27133,
|
||||
"native-worker-error.ts": 2958,
|
||||
"native-worker-exception-state.ts": 3945,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { describe, expect, test } from "vitest";
|
||||
import ts5 from "typescript";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
import { corpusSelect, shardSuffix } from "./shard.js";
|
||||
import { oracleEnvKey } from "./oracle-env.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
@@ -238,8 +239,8 @@ function programInputs(file: string): string[] {
|
||||
// Node's verdict for a corpus program is a pure function of the program bytes,
|
||||
// the shims, and the Node build (corpus stdout is deterministic by
|
||||
// construction — it must be identical to a non-Node native binary's output). So
|
||||
// cache it, keyed by all of those plus the invocation shape (SCRIPTC_TEST_ENV
|
||||
// and the cwd). Only the SPAWN is skipped: the native side always runs live
|
||||
// cache it, keyed by all of those plus the invocation shape (the inherited
|
||||
// environment, see oracle-env.ts, and the cwd). Only the SPAWN is skipped: the native side always runs live
|
||||
// and the comparison itself never changes. SCRIPTC_NO_CACHE=1 (or an unset
|
||||
// SCRIPTC_CACHE_DIR) disables the cache in both directions — no reads, no writes.
|
||||
// Storage shares the compile cache's root and its LRU sweep (see native-toolchain.ts).
|
||||
@@ -265,7 +266,7 @@ function oracleKeyBase(): Promise<string> {
|
||||
.update(readFileSync(fileURLToPath(comptimeShim)))
|
||||
.update("\0")
|
||||
.update("\0")
|
||||
.update(process.env["SCRIPTC_TEST_ENV"] ?? "")
|
||||
.update(oracleEnvKey(process.env))
|
||||
.update("\0")
|
||||
.update(process.cwd())
|
||||
.update("\0")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { oracleEnvKey } from "./oracle-env.js";
|
||||
|
||||
test("the oracle key changes with any inherited variable", () => {
|
||||
const base = { PATH: "/bin", SCRIPTC_TEST_ENV: "from-harness" };
|
||||
expect(oracleEnvKey({ ...base, NODE_ENV: "production" })).not.toBe(
|
||||
oracleEnvKey({ ...base, NODE_ENV: "test" }),
|
||||
);
|
||||
expect(oracleEnvKey({ ...base, NODE_ENV: "test" })).not.toBe(oracleEnvKey(base));
|
||||
expect(oracleEnvKey({ ...base, FOO: "a=b" })).not.toBe(
|
||||
oracleEnvKey({ ...base, FOO: "a", B: "" }),
|
||||
);
|
||||
});
|
||||
|
||||
test("the oracle key ignores the runner's worker numbers and variable order", () => {
|
||||
expect(oracleEnvKey({ A: "1", B: "2", VITEST_WORKER_ID: "1", VITEST_POOL_ID: "1" })).toBe(
|
||||
oracleEnvKey({
|
||||
VITEST_POOL_ID: "7",
|
||||
B: "2",
|
||||
TINYPOOL_WORKER_ID: "3",
|
||||
A: "1",
|
||||
VITEST_WORKER_ID: "9",
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/* The environment part of the differential oracle cache key. The Node
|
||||
* oracle runs with the harness's inherited environment, so a program whose
|
||||
* output reads a variable (NODE_ENV, SCRIPTC_TEST_ENV, ...) must not reuse a
|
||||
* verdict recorded under a different value. The variables that number the
|
||||
* test runner's own workers differ between workers of one run and are left
|
||||
* out, so every worker shares the cache. */
|
||||
const WORKER_VARIABLES = new Set(["VITEST_POOL_ID", "VITEST_WORKER_ID", "TINYPOOL_WORKER_ID"]);
|
||||
|
||||
export function oracleEnvKey(env: NodeJS.ProcessEnv): string {
|
||||
return Object.keys(env)
|
||||
.filter((name) => !WORKER_VARIABLES.has(name) && env[name] !== undefined)
|
||||
.sort()
|
||||
.map((name) => `${name}=${env[name]}\0`)
|
||||
.join("");
|
||||
}
|
||||
Reference in New Issue
Block a user