diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx
index 8f17f6a98..11737da59 100644
--- a/docs/content/docs/contracts.mdx
+++ b/docs/content/docs/contracts.mdx
@@ -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";
diff --git a/docs/content/docs/threads.mdx b/docs/content/docs/threads.mdx
index 15a3eb102..7b394e09f 100644
--- a/docs/content/docs/threads.mdx
+++ b/docs/content/docs/threads.mdx
@@ -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`.
diff --git a/docs/content/limitations/builtin-apis/regular-expressions.mdx b/docs/content/limitations/builtin-apis/regular-expressions.mdx
index 4578cfc93..a765bbe3c 100644
--- a/docs/content/limitations/builtin-apis/regular-expressions.mdx
+++ b/docs/content/limitations/builtin-apis/regular-expressions.mdx
@@ -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.
diff --git a/docs/content/limitations/native-worker-threads.mdx b/docs/content/limitations/native-worker-threads.mdx
index 972eadeca..0510936d6 100644
--- a/docs/content/limitations/native-worker-threads.mdx
+++ b/docs/content/limitations/native-worker-threads.mdx
@@ -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.
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index 1024b5382..1c1969968 100644
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ b/docs/src/generated/node-v24-compatibility-meta.json
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
- "artifactVersion": "737b4d1da8285efa2be0",
+ "artifactVersion": "823524db8f12948bdc73",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index 5c19a408f..3a986ebf7 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -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"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index 6601c2d3d..565aadbb3 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -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"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index b01bdf292..d3d1addfe 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -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"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index 9ee474476..b86e033f9 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -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"]},
diff --git a/native/llvm-codegen/README.md b/native/llvm-codegen/README.md
index 5b8f38f2a..8debaeeaf 100644
--- a/native/llvm-codegen/README.md
+++ b/native/llvm-codegen/README.md
@@ -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
` 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 ` 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.
diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp
index b39d2c1bf..a5f87e4b6 100644
--- a/native/llvm-codegen/src/emit.cpp
+++ b/native/llvm-codegen/src/emit.cpp
@@ -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 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());
+ 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 States(MF.getNumBlockIDs(), State::New);
+ SmallVector,
+ 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 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(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
@@ -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 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(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 Names(Count);
for (size_t I = 0; I < Count; ++I)
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index 790d80d80..b9475d51f 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -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;
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. */
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index 86a8a945e..d16d8244c 100644
--- a/packages/compiler/src/backend/llvm/dyn.ts
+++ b/packages/compiler/src/backend/llvm/dyn.ts
@@ -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)!);
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index f4a0fda6c..c0b3725b7 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -945,6 +945,7 @@ export const LIB_FN_SYMS: Record = {
"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",
diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts
index 9a7b4a37a..eaae1b531 100644
--- a/packages/compiler/src/diagnostics/diagnostic.ts
+++ b/packages/compiler/src/diagnostics/diagnostic.ts
@@ -411,7 +411,7 @@ export const UNSUPPORTED: Record = {
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, $ templates, \\k), .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, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates",
},
// SC1121 (stateful g/y regex tests) shipped — code retired, do not reuse.
};
diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts
index b535b8a5d..037b3e5a7 100644
--- a/packages/compiler/src/frontend/lowering/builtin-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts
@@ -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(
diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts
index 1061f1389..2311e0f96 100644
--- a/packages/compiler/src/frontend/lowering/builtins/workers.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts
@@ -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(
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 8dd9da069..8856ae9b5 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -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",
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 61dfcef3b..391dd7c88 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -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 = 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
* `(?...)` and `\k` backreferences compile — libregexp
* executes them natively, replace templates resolve `$` 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) };
}
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 9ac572945..b93490f9d 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -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 },
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index a5e0e5044..9ba7a7321 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -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
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index e85bd82b9..388abf0a7 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -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);
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index 9a0188cf3..683df3777 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -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, $ templates, \\k), .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, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates"
},
{
"id": "diagnostic.sc2001",
diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts
index fb00116cc..1c5f9e3e7 100644
--- a/packages/compiler/test/native-codegen-integration.test.ts
+++ b/packages/compiler/test/native-codegen-integration.test.ts
@@ -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",
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5857-regexp-unicode-sets.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5857-regexp-unicode-sets.ts.json
new file mode 100644
index 000000000..20742aa5a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5857-regexp-unicode-sets.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5857-regexp-unicode-sets.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5915-dyn-error-string-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5915-dyn-error-string-conversions.ts.json
new file mode 100644
index 000000000..032b5c5ba
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5915-dyn-error-string-conversions.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5915-dyn-error-string-conversions.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-worker-uncloneable.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-worker-uncloneable.mjs.json
new file mode 100644
index 000000000..93337e4a0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5990-worker-uncloneable.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5990-worker-uncloneable.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5991-worker-async-dispose.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5991-worker-async-dispose.mjs.json
new file mode 100644
index 000000000..bed1b18a9
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5991-worker-async-dispose.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5991-worker-async-dispose.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-discarded-heaps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-discarded-heaps.ts.json
deleted file mode 100644
index b7dd8fd17..000000000
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/native-worker-discarded-heaps.ts.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "order": [
- "/tests/corpus/native-worker-discarded-heaps.ts"
- ],
- "diags": []
-}
diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c
index ffe98bcf8..76d056d7c 100644
--- a/packages/runtime/src/scr_alloc.c
+++ b/packages/runtime/src/scr_alloc.c
@@ -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
diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c
index 032c41f7f..b3228ce89 100644
--- a/packages/runtime/src/scr_alloc.test.c
+++ b/packages/runtime/src/scr_alloc.test.c
@@ -429,273 +429,6 @@ static void check_trim(void) {
}
#endif
-#if defined(SCR_WORKERS) && SCR_SMALL_ALLOC && defined(__linux__)
-#include
-#include
-#include
-#include
-#include
-
-/* 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
diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h
index 08a4d79b1..416696717 100644
--- a/packages/runtime/src/scr_dyn.h
+++ b/packages/runtime/src/scr_dyn.h
@@ -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);
diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c
index 59cf34a10..a3085399f 100644
--- a/packages/runtime/src/scr_dyn_invoke.c
+++ b/packages/runtime/src/scr_dyn_invoke.c
@@ -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]");
diff --git a/packages/runtime/src/scr_itable.c b/packages/runtime/src/scr_itable.c
index 4842bf79c..08db948b5 100644
--- a/packages/runtime/src/scr_itable.c
+++ b/packages/runtime/src/scr_itable.c
@@ -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);
diff --git a/packages/runtime/src/scr_itable.test.c b/packages/runtime/src/scr_itable.test.c
index 5e5f22bc9..0f218806b 100644
--- a/packages/runtime/src/scr_itable.test.c
+++ b/packages/runtime/src/scr_itable.test.c
@@ -8,18 +8,13 @@
#include
#include
-/* 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;
}
diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c
index 6633f4840..90b8f822e 100644
--- a/packages/runtime/src/scr_json.c
+++ b/packages/runtime/src/scr_json.c
@@ -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", "#