diff --git a/AGENTS.md b/AGENTS.md
index 26305f340..9be4eb548 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.
+Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
+
Test location follows scope:
- Co-locate white-box unit tests with implementation files under
diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index 5c1b5a9b5..d1600346c 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -54,7 +54,7 @@ fib.s
Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file.
-The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v6 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime.
+The IR and LLVM source outputs require only Node. On supported macOS, Linux, and Windows hosts, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined scr_* runtime symbols and a required scr_runtime_abi_v7 marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by --print=native-link-info. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, main entry, installed precompiled runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See Native Program Objects for complete C-driver and direct-linker examples. --emit=exe is the default. LLVM builds emit the program object through the helper and link precompiled runtime objects; runtime development with AddressSanitizer compiles an instrumented C runtime.
## scriptc run
diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx
index 985e9bd75..c0a4587dd 100644
--- a/docs/content/docs/how-it-works.mdx
+++ b/docs/content/docs/how-it-works.mdx
@@ -68,7 +68,7 @@ Without `-o`, artifacts are written to `.scriptc/` next to the input. Different
### External linking
-Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v6` checks runtime compatibility at link time.
+Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v7` checks runtime compatibility at link time.
`--print=native-link-info` emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See [Native Program Objects](/docs/native-objects) for examples.
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index ee30d29b5..6cdc2464d 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -64,7 +64,7 @@ A declaration in the standard library does not guarantee that an API compiles. U
- **FileHandle and descriptor I/O:** Static reads and writes accept Buffer and Uint8Array windows, numeric positions, and UTF-8 strings. FileHandle reads also accept options records and allocate a buffer when omitted. Vector I/O, truncation, syncing, and permission changes compile. FileHandle streams, `readLines`, ownership changes, timestamps, and bigint Stats remain unsupported. Dynamic descriptor I/O accepts other ArrayBuffer views and Buffer encodings; read positions outside the safe integer range are refused. File operations complete synchronously before their callbacks or promise continuations. Descriptor permission changes on Windows throw `ENOSYS`.
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support MD5, SHA-1, SHA-224, SHA-256, SHA-384, and SHA-512. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. KeyObject inputs, other digests, and cipher or public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. Reads, resets, and assignments used as values are unsupported.
-- **Builtin module lookup:** `process.getBuiltinModule()` exposes native subsets of `path`, `path/posix`, `path/win32`, `os`, and main-thread `worker_threads` metadata. Stored module references and functions retain identity. Other modules and exports, including `Worker`, throw `SC2020`. `process.versions` is a shared dictionary containing `node` and `openssl`; other component versions are absent unless defined by the program.
+- **Builtin module lookup:** `process.getBuiltinModule()` exposes native subsets of `path`, `path/posix`, `path/win32`, `os`, and current-thread `worker_threads` metadata. Stored module references and functions retain identity. Other modules and exports, including stored `Worker` constructors, throw `SC2020`. Direct `Worker` imports use the native worker support described below. `process.versions` is a shared dictionary containing `node` and `openssl`; other component versions are absent unless defined by the program.
- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, Float16 arrays, and BigInt arrays are unsupported. Numeric typed arrays and Buffers preserve their kind and shared storage through `unknown`, including when nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage.
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion. Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
- **Intl.Segmenter:** Default Unicode grapheme segmentation, iteration, and `containing()` compile. Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
@@ -94,6 +94,16 @@ Synchronous and promise filesystem APIs with trailing arguments that affect beha
Native error-first filesystem callbacks can be stored and passed to platform adapters. Reads support Buffer and UTF-8 results; writes preserve string flags and modes; copy supports regular files and recursive directories. Reads and writes reject already-aborted signals, but native file operations complete synchronously before the deferred callback, so cancellation cannot interrupt I/O in progress. Symbolic-link copy, copy filters and clone modes, recursive glob patterns, glob exclusions, recursive directory reads, and Dirent results report a native limitation.
+### Native worker threads
+
+`node:worker_threads` supports native threads in static executables. 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` and `argv`; runtime paths, `eval`, loaders, custom stdio, shared environments, resource limits, and transfer lists are unsupported. Worker argument strings containing null bytes are also unsupported.
+
+Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. 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. Functions, proxies, other native handles, and nonempty transfer lists are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread.
+
+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. `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.
+
+Workers are unsupported in dynamic executables, WASI, compiled libraries, and executables that use foreign-thread native callbacks. Ordinary callbacks that run on their owning script thread remain supported. Shared byte storage cannot expose unmanaged FFI pointers; copy it into an ordinary Buffer before passing it to native code.
+
### Child-process workers
`child_process.fork()` requires a worker path resolved at build time from `new URL("./worker.ts", import.meta.url)` or `fileURLToPath(new URL("./worker.ts", import.meta.url))`. Never-reassigned `const` aliases and statically resolvable template parts are accepted. The worker and its imports are embedded in the executable, which re-executes itself to start that module. Worker arguments begin at `process.argv[2]`.
diff --git a/docs/content/docs/native-objects.mdx b/docs/content/docs/native-objects.mdx
index 636890c5a..54f1efacb 100644
--- a/docs/content/docs/native-objects.mdx
+++ b/docs/content/docs/native-objects.mdx
@@ -8,7 +8,7 @@ title: "Native Program Objects"
The external object ABI is **experimental**. Its `scr_*` function and data surface may change before 1.0, so consumers must use the exact runtime-pack version reported by the same compiler installation. This is stricter than semver compatibility.
-The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v6`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility.
+The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v7`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility.
## Machine-readable link information
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index c035564dd..4b0f2a929 100644
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ b/docs/src/generated/node-v24-compatibility-meta.json
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
- "artifactVersion": "3e2ea82c2a32fb0f484c",
+ "artifactVersion": "32b04265083c89d00ea3",
"rowCount": 3662
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index 983b584bc..643394fd5 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -1346,9 +1346,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
- "partial": 4,
+ "partial": 24,
"refused": 0,
- "not-implemented": 62,
+ "not-implemented": 42,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -96738,9 +96738,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"
},
"dynamic": {
"status": "partial",
@@ -96766,9 +96766,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"
},
"dynamic": {
"status": "partial",
@@ -96794,9 +96794,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"
},
"dynamic": {
"status": "partial",
@@ -96822,9 +96822,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"
},
"dynamic": {
"status": "partial",
@@ -96850,9 +96850,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"
},
"dynamic": {
"status": "partial",
@@ -96878,9 +96878,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"
},
"dynamic": {
"status": "partial",
@@ -96906,9 +96906,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"
},
"dynamic": {
"status": "partial",
@@ -96934,9 +96934,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"
},
"dynamic": {
"status": "not-implemented",
@@ -96962,9 +96962,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97018,9 +97018,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"
},
"dynamic": {
"status": "refused",
@@ -97130,9 +97130,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97158,9 +97158,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97242,9 +97242,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97270,9 +97270,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97495,7 +97495,7 @@
},
"static": {
"status": "partial",
- "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
@@ -97550,9 +97550,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97578,9 +97578,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97606,9 +97606,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97662,9 +97662,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"
},
"dynamic": {
"status": "not-implemented",
@@ -97943,7 +97943,7 @@
},
"static": {
"status": "partial",
- "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
@@ -97970,9 +97970,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"
},
"dynamic": {
"status": "partial",
@@ -98055,7 +98055,7 @@
},
"static": {
"status": "partial",
- "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
},
"dynamic": {
@@ -98110,9 +98110,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"
},
"dynamic": {
"status": "partial",
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index 0493fbe88..8bd3a5d37 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -8,8 +8,8 @@
"tiers": {
"static": {
"replace-refusal": 42,
- "verify-gap": 2447,
- "audit-partial": 717,
+ "verify-gap": 2427,
+ "audit-partial": 737,
"classify": 2,
"implement": 80
},
@@ -107505,12 +107505,16 @@
"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.MessagePort",
+ "tests": [
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -107540,12 +107544,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.port.close",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -107575,12 +107581,17 @@
"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.port.postMessage",
+ "tests": [
+ "tests/corpus/native-worker-message-graphs.ts",
+ "tests/corpus/native-worker-shared-views.ts",
+ "tests/corpus/native-worker-clone-errors.ts",
+ "tests/corpus/native-worker-delayed-messages.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -107610,12 +107621,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.port.hasRef",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -107645,12 +107658,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.port.ref",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -107680,12 +107695,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.port.start",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -107715,12 +107732,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.port.unref",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -107750,12 +107769,15 @@
"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.'close'",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-messages.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -107783,12 +107805,15 @@
"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.'message'",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-messages.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -107849,12 +107874,17 @@
"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/native-worker-messages.ts",
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-environment.ts",
+ "tests/corpus/native-worker-roots/main.ts"
+ ]
},
"dynamic": {
"status": "refused",
@@ -107981,12 +108011,17 @@
"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.postMessage",
+ "tests": [
+ "tests/corpus/native-worker-message-graphs.ts",
+ "tests/corpus/native-worker-shared-views.ts",
+ "tests/corpus/native-worker-clone-errors.ts",
+ "tests/corpus/native-worker-delayed-messages.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108014,12 +108049,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.ref",
+ "tests": [
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108113,12 +108150,17 @@
"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.terminate",
+ "tests": [
+ "tests/corpus/native-worker-terminate-loop.ts",
+ "tests/corpus/native-worker-terminate-wait.ts",
+ "tests/corpus/native-worker-terminate-suspended.ts",
+ "tests/corpus/native-worker-loop-cancellation.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108146,12 +108188,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.unref",
+ "tests": [
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108416,9 +108460,11 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
- "source": "surface-manifest:node-builtin.worker_threads.threadId",
+ "source": "compiler-feature:worker_threads.threadId",
"tests": [
- "tests/corpus/957-builtins-namespace.ts"
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
]
},
"dynamic": {
@@ -108482,12 +108528,16 @@
"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.'error'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108515,12 +108565,16 @@
"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.'exit'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108548,12 +108602,16 @@
"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.'message'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108614,12 +108672,16 @@
"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.'online'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -108956,9 +109018,11 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
- "source": "surface-manifest:node-builtin.worker_threads.isMainThread",
+ "source": "compiler-feature:worker_threads.isMainThread",
"tests": [
- "tests/corpus/957-builtins-namespace.ts"
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
]
},
"dynamic": {
@@ -108989,12 +109053,16 @@
"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.parentPort",
+ "tests": [
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -109098,9 +109166,11 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
- "source": "surface-manifest:node-builtin.worker_threads.threadId",
+ "source": "compiler-feature:worker_threads.threadId",
"tests": [
- "tests/corpus/957-builtins-namespace.ts"
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
]
},
"dynamic": {
@@ -109164,12 +109234,16 @@
"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.workerData",
+ "tests": [
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "partial",
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index 27a1f8f58..62073fb98 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -1467,9 +1467,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
- "partial": 4,
+ "partial": 24,
"refused": 0,
- "not-implemented": 62,
+ "not-implemented": 42,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -125858,7 +125858,22 @@
"status": "partial",
"evidence": "derived:descendants",
"tests": [
- "tests/corpus/957-builtins-namespace.ts"
+ "tests/corpus/native-worker-clone-errors.ts",
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-delayed-messages.ts",
+ "tests/corpus/native-worker-environment.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/native-worker-loop-cancellation.ts",
+ "tests/corpus/native-worker-message-graphs.ts",
+ "tests/corpus/native-worker-messages.ts",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-roots/main.ts",
+ "tests/corpus/native-worker-shared-views.ts",
+ "tests/corpus/native-worker-terminate-loop.ts",
+ "tests/corpus/native-worker-terminate-suspended.ts",
+ "tests/corpus/native-worker-terminate-wait.ts"
]
},
"dynamic": {
@@ -126188,8 +126203,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.MessagePort",
+ "tests": [
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -126218,8 +126238,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.port.close",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -126248,8 +126271,14 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.port.postMessage",
+ "tests": [
+ "tests/corpus/native-worker-message-graphs.ts",
+ "tests/corpus/native-worker-shared-views.ts",
+ "tests/corpus/native-worker-clone-errors.ts",
+ "tests/corpus/native-worker-delayed-messages.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -126332,8 +126361,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.port.hasRef",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -126362,8 +126394,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.port.ref",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -126392,8 +126427,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.port.start",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -126422,8 +126460,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.port.unref",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -126452,8 +126493,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.'close'",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-messages.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -126479,8 +126524,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.'message'",
+ "tests": [
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-messages.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -126533,8 +126582,14 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.Worker",
+ "tests": [
+ "tests/corpus/native-worker-messages.ts",
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-environment.ts",
+ "tests/corpus/native-worker-roots/main.ts"
+ ]
},
"dynamic": {
"status": "refused",
@@ -126641,8 +126696,14 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker.postMessage",
+ "tests": [
+ "tests/corpus/native-worker-message-graphs.ts",
+ "tests/corpus/native-worker-shared-views.ts",
+ "tests/corpus/native-worker-clone-errors.ts",
+ "tests/corpus/native-worker-delayed-messages.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -126668,8 +126729,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker.ref",
+ "tests": [
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -126749,8 +126813,14 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker.terminate",
+ "tests": [
+ "tests/corpus/native-worker-terminate-loop.ts",
+ "tests/corpus/native-worker-terminate-wait.ts",
+ "tests/corpus/native-worker-terminate-suspended.ts",
+ "tests/corpus/native-worker-loop-cancellation.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -126776,8 +126846,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker.unref",
+ "tests": [
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -126996,9 +127069,11 @@
},
"static": {
"status": "partial",
- "evidence": "surface-manifest:node-builtin.worker_threads.threadId",
+ "evidence": "compiler-feature:worker_threads.threadId",
"tests": [
- "tests/corpus/957-builtins-namespace.ts"
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
]
},
"dynamic": {
@@ -127055,8 +127130,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.'error'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -127082,8 +127162,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.'exit'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -127109,8 +127194,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.'message'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -127163,8 +127253,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.'online'",
+ "tests": [
+ "tests/corpus/native-worker-references.ts",
+ "tests/corpus/native-worker-error.ts",
+ "tests/corpus/native-worker-exit-listeners.ts"
+ ]
},
"dynamic": {
"status": "not-implemented",
@@ -127446,9 +127541,11 @@
},
"static": {
"status": "partial",
- "evidence": "surface-manifest:node-builtin.worker_threads.isMainThread",
+ "evidence": "compiler-feature:worker_threads.isMainThread",
"tests": [
- "tests/corpus/957-builtins-namespace.ts"
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
]
},
"dynamic": {
@@ -127478,8 +127575,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.parentPort",
+ "tests": [
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "partial",
@@ -127569,9 +127671,11 @@
},
"static": {
"status": "partial",
- "evidence": "surface-manifest:node-builtin.worker_threads.threadId",
+ "evidence": "compiler-feature:worker_threads.threadId",
"tests": [
- "tests/corpus/957-builtins-namespace.ts"
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
]
},
"dynamic": {
@@ -127628,8 +127732,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker_threads.workerData",
+ "tests": [
+ "tests/corpus/native-worker-commonjs.cjs",
+ "tests/corpus/native-worker-port-lifetime.ts",
+ "tests/corpus/native-worker-references.ts"
+ ]
},
"dynamic": {
"status": "partial",
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index dc166438a..da1c5128e 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -106,7 +106,7 @@
"node-builtin.util.isDeepStrictEqual": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"],
"node-builtin.util.types": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"],
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
- "node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts"],
+ "node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts"],
"stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.map": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/537-map-iter-drains.ts"],
@@ -118,6 +118,14 @@
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
+ {"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts"]},
+ {"chapter": "worker_threads", "symbols": ["MessagePort", "parentPort", "worker_threads.workerData", "isMainThread", "threadId"], "status": "partial", "evidence": ["tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-references.ts"]},
+ {"chapter": "worker_threads", "symbols": ["worker.postMessage", "port.postMessage"], "status": "partial", "evidence": ["tests/corpus/native-worker-message-graphs.ts", "tests/corpus/native-worker-shared-views.ts", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-delayed-messages.ts"]},
+ {"chapter": "worker_threads", "symbols": ["worker.terminate"], "status": "partial", "evidence": ["tests/corpus/native-worker-terminate-loop.ts", "tests/corpus/native-worker-terminate-wait.ts", "tests/corpus/native-worker-terminate-suspended.ts", "tests/corpus/native-worker-loop-cancellation.ts"]},
+ {"chapter": "worker_threads", "symbols": ["worker.ref", "worker.unref"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts"]},
+ {"chapter": "worker_threads", "symbols": ["port.close", "port.start", "port.ref", "port.unref", "port.hasRef"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts"]},
+ {"chapter": "worker_threads", "parent": "Class: `Worker`", "signatures": ["Event: `'online'`", "Event: `'message'`", "Event: `'error'`", "Event: `'exit'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", "tests/corpus/native-worker-exit-listeners.ts"]},
+ {"chapter": "worker_threads", "parent": "Class: `MessagePort`", "signatures": ["Event: `'message'`", "Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-messages.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{ "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] },
{ "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] },
diff --git a/packages/cli/README.md b/packages/cli/README.md
index f41f220d0..44fd5917d 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -34,7 +34,7 @@ Builds use a bounded persistent cache. Unchanged source can reuse the validated
- `scriptc run ` — compile and run
- `scriptc coverage ` — what compiles statically, and why the rest doesn't
-`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v6` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available.
+`scriptc build app.ts --emit=ir|llvm|asm|obj` selects serialized typed IR, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v7` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available.
For embedder-hosted modules that are not installed npm packages, coverage can map an exact bare specifier to a local declaration with repeatable `--external-types ` options. This is analysis-only: the types unblock application measurement, while runtime module uses remain reported as blockers.
diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts
index 9d61c4466..73e429be8 100644
--- a/packages/cli/test/native-link-info.test.ts
+++ b/packages/cli/test/native-link-info.test.ts
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
- runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" },
+ runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
() => null,
(failure: { stderr?: string }) => failure,
);
- expect(error?.stderr).toContain("scr_runtime_abi_v6");
+ expect(error?.stderr).toContain("scr_runtime_abi_v7");
});
});
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index dcdb8fc8a..41378ff9b 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -601,7 +601,7 @@ interface BufferConstructor {
from(data: string, encoding?: BufferEncoding): Buffer;
/* The ArrayBuffer form is the VIEW construction: x.buffer with an
* optional byte offset/length shares x's storage (the subarray rule). */
- from(data: ArrayBuffer, byteOffset?: number, length?: number): Buffer;
+ from(data: ArrayBuffer | SharedArrayBuffer, byteOffset?: number, length?: number): Buffer;
from(data: Uint8Array | readonly number[]): Buffer;
alloc(size: number, fill?: string | number | Uint8Array, encoding?: BufferEncoding): Buffer;
compare(buf1: Uint8Array, buf2: Uint8Array): number;
@@ -4096,23 +4096,61 @@ declare module "node:dns" {
export * from "dns";
}
-/* node:worker_threads — the MAIN-THREAD slice only. A compiled binary is
- * always the main thread (no JS-engine thread machinery exists), so
- * isMainThread lowers to `true` and threadId to 0 — Node's main-thread
- * answers exactly. Worker itself is declared surface without a lowering:
- * constructing one fences at the site; the CLASS as a value participates
- * in identity-only flows. */
+/* node:worker_threads — native workers use statically compiled entry points.
+ * Lowering and runtime diagnostics define the supported options and methods. */
declare module "worker_threads" {
export const isMainThread: boolean;
+ export const isInternalThread: boolean;
export const threadId: number;
- export class Worker {
- constructor(filename: string | URL, options?: unknown);
- on(event: string, listener: (...args: unknown[]) => void): void;
- postMessage(value: unknown): void;
- terminate(): Promise;
+ export interface WorkerOptions {
+ workerData?: unknown;
+ transferList?: readonly unknown[];
+ argv?: readonly unknown[];
+ name?: string;
+ [option: string]: unknown;
}
- export const parentPort: unknown;
- export const workerData: unknown;
+ export class Worker {
+ constructor(filename: string | URL, options?: WorkerOptions);
+ readonly threadId: number;
+ on(event: "online", listener: () => void): this;
+ on(event: "message", listener: (value: any) => void): this;
+ on(event: "error", listener: (error: Error) => void): this;
+ on(event: "exit", listener: (code: number) => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ once(event: string, listener: (...args: any[]) => void): this;
+ off(event: string, listener: (...args: any[]) => void): this;
+ addListener(event: string, listener: (...args: any[]) => void): this;
+ removeListener(event: string, listener: (...args: any[]) => void): this;
+ prependListener(event: string, listener: (...args: any[]) => void): this;
+ prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+ postMessage(value: unknown, transferList?: readonly unknown[]): void;
+ ref(): void;
+ unref(): void;
+ terminate(): Promise;
+ removeAllListeners(event?: string): this;
+ listenerCount(event: string, listener?: (...args: any[]) => void): number;
+ }
+ export class MessagePort {
+ on(event: "message", listener: (value: any) => void): this;
+ on(event: "close", listener: () => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ once(event: string, listener: (...args: any[]) => void): this;
+ off(event: string, listener: (...args: any[]) => void): this;
+ addListener(event: string, listener: (...args: any[]) => void): this;
+ removeListener(event: string, listener: (...args: any[]) => void): this;
+ prependListener(event: string, listener: (...args: any[]) => void): this;
+ prependOnceListener(event: string, listener: (...args: any[]) => void): this;
+ postMessage(value: unknown, transferList?: readonly unknown[]): void;
+ start(): void;
+ close(callback?: () => void): void;
+ ref(): void;
+ unref(): void;
+ hasRef(): boolean;
+ removeAllListeners(event?: string): this;
+ listenerCount(event: string, listener?: (...args: any[]) => void): number;
+ }
+ export const parentPort: MessagePort | null;
+ export const workerData: any;
}
declare module "node:worker_threads" {
export * from "worker_threads";
diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts
index 4ac5a050d..0fe62630c 100644
--- a/packages/compiler/src/backend/cycle-analysis.test.ts
+++ b/packages/compiler/src/backend/cycle-analysis.test.ts
@@ -19,7 +19,7 @@ import { computeTraced as llvmTraced } from "./llvm/shapes.js";
const loc = { file: "cycles.ts", start: 0, end: 1 };
function module(): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [{ name: "main", locals: [], params: [], returnType: VOID, body: [], loc }],
diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts
index 669d6471b..77efa11dd 100644
--- a/packages/compiler/src/backend/executable-features.ts
+++ b/packages/compiler/src/backend/executable-features.ts
@@ -11,6 +11,7 @@ import type { NativeLinkFeatures } from "./native-link-info.js";
export function executableLinkFeatures(mod: IrModule, dynamic: boolean): NativeLinkFeatures {
const features = moduleRuntimeFeatures(mod);
return {
+ ...(mod.workers ? { workers: true } : {}),
dynamic,
regex: features.regex,
copying: features.copying,
diff --git a/packages/compiler/src/backend/llvm/byte-numbers.ts b/packages/compiler/src/backend/llvm/byte-numbers.ts
index abdb71f70..07f0142bd 100644
--- a/packages/compiler/src/backend/llvm/byte-numbers.ts
+++ b/packages/compiler/src/backend/llvm/byte-numbers.ts
@@ -46,6 +46,7 @@ export function emitByteNumber(
little: string,
fallback: () => LlValue,
): LlValue {
+ if (host.mod.workers) return fallback();
const B = host.B,
size = host.sizeType;
const slow = B.newLabel("bytes.number.slow"),
diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts
index b0aed0a83..d9029a644 100644
--- a/packages/compiler/src/backend/llvm/classes.test.ts
+++ b/packages/compiler/src/backend/llvm/classes.test.ts
@@ -9,7 +9,7 @@ const loc = { file: "class-membership.ts", start: 0, end: 1 };
test("leaf layouts keep direct teardown while recursive layouts share the depth budget", () => {
const linked = { kind: "object" as const, className: "Linked" };
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
@@ -61,7 +61,7 @@ function graph() {
{ name: "FlippedString", base: "PairString", genericOf: "Flipped" },
];
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [],
@@ -95,7 +95,7 @@ test.each(["i32", "i64"])(
expect(helper).toContain("label %ordinary");
expect(helper).toContain("or i1");
const ordinary: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [],
diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts
index 782e64464..b4b6a72bd 100644
--- a/packages/compiler/src/backend/llvm/classes.ts
+++ b/packages/compiler/src/backend/llvm/classes.ts
@@ -661,6 +661,7 @@ export function emitClassObjDefs(
classObjs: Map,
fnByName: Map,
llType: (t: IrType) => string,
+ threadLocal = false,
): string[] {
const out: string[] = [];
for (const [className, { nameSym }] of classObjs) {
@@ -744,7 +745,7 @@ export function emitClassObjDefs(
` call void @${mangleFunction(`%${className}.constructor`)}(${[`ptr %r`, ...ctorArgs].join(", ")})`,
` ret ptr %o`,
`}`,
- `@${mangleClassObj(className)} = internal global %ScrClassObj ` +
+ `@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` +
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? 0}, ptr null, ptr null } ; class ${className}`,
``,
);
diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts
index 4ec287744..a49a4e089 100644
--- a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts
+++ b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts
@@ -66,7 +66,7 @@ function fixture(): IrModule {
loc,
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [predicate, consumer, entry],
diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts
index ad9fb4d04..1ef04762e 100644
--- a/packages/compiler/src/backend/llvm/emitter.test.ts
+++ b/packages/compiler/src/backend/llvm/emitter.test.ts
@@ -59,7 +59,7 @@ function moduleFor(
loc,
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [
@@ -184,7 +184,7 @@ const text = (obj: IrExpr): IrExpr => ({
function work(expr: IrExpr, parameter: IrType, boxed = false, tdz = false): string {
const module: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: receiverLoc.file,
entry: "main",
records: [
@@ -355,7 +355,7 @@ function sharedFieldModule(prefixes: IrType[][], fieldType: IrType): IrModule {
],
}));
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
records,
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 4011a3c20..476808e93 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -678,7 +678,8 @@ export class LlEmitter {
}
const mt = computeMayThrow(mod);
this.mayThrow = mt.fns;
- this.indirectMayThrow = mt.indirect;
+ if (mod.workers) for (const fn of mod.functions) this.mayThrow.add(fn.name);
+ this.indirectMayThrow = mt.indirect || mod.workers === true;
for (const cls of mod.classes ?? []) {
for (const m of cls.methods ?? []) {
if (this.mayThrow.has(`%${cls.name}.${m}`)) this.mayThrowMethods.add(m);
@@ -835,7 +836,9 @@ export class LlEmitter {
if (adapter.global !== null)
globals.push(`@${adapter.global} = internal thread_local global ptr null`);
if (adapter.table !== null) {
- globals.push(`@${adapter.table} = internal global %ScrFfiTable zeroinitializer`);
+ globals.push(
+ `@${adapter.table} = internal ${this.mod.workers ? "thread_local " : ""}global %ScrFfiTable zeroinitializer`,
+ );
}
const params = cb.params.flatMap((param, i): string[] => {
if (isFfiContextParam(param)) return [`ptr %ctx`];
@@ -1382,7 +1385,8 @@ export class LlEmitter {
// with neither keep the atexit path, so their listener timing is
// unchanged.
const hasRefGlobals = globals.some((g) => isRefCounted(g.type)) || fnValueProps.length > 0;
- const inlineExitListeners = usesEvents && (hasRefGlobals || this.ffiHasRetainedCallback);
+ const inlineExitListeners =
+ usesEvents && (this.mod.workers || hasRefGlobals || this.ffiHasRetainedCallback);
this.declare(`declare i32 @scr_exit_code_hint_get()`);
if (inlineExitListeners) {
this.declare(`declare void @scr_run_exit_listeners(double)`);
@@ -1564,11 +1568,11 @@ export class LlEmitter {
// scr_runtime.h's natural alignment: kind at 8, f64 at 16, b at 24,
// payload at 32).
`%ScrCaught = type { ${this.sizeType}, i32, double, i8, ptr, ptr, ptr, ptr }`,
- // ScrBytes { rc, len, elem(i32+pad), data, backing }. Indexed
+ // ScrBytes { rc, len, elem(i32+pad), data, backing, brands, shared }. Indexed
// typed-array access GEPs through this directly: the IR type already
// fixes elem, so the hot path needs neither a runtime kind load nor
// the generic scr_bytes_get/set call.
- `%ScrBytes = type { ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, i8 }`,
+ `%ScrBytes = type { ${this.sizeType}, ${this.sizeType}, i32, ptr, ptr, i8, i8, i8, ptr }`,
// The capture box { rc, kind, obj_retain, obj_release, obj_trace,
// slot } — TDZ reads peek the payload slot (offset 40) directly.
`%ScrBox = type { ${this.sizeType}, i32, ptr, ptr, ptr, i64 }`,
@@ -1589,12 +1593,14 @@ export class LlEmitter {
// objects compiled with -DSCR_THREAD_INSTANCES. Immutable interned
// data (string literals, unit arms, template arrays, vtables) stays
// shared.
- const tl = this.mod.lib?.threadInstances === true ? "thread_local " : "";
+ const tl =
+ this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : "";
out.push(
``,
`@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`,
`declare void @scr_init()`,
`declare void @scr_lib_init(i32, ptr)`,
+ ...(this.mod.workers ? [`declare void @scr_runtime_workers_v7()`] : []),
...(this.runtimeAbiMarker && this.mod.lib === undefined
? [`declare void @${RUNTIME_ABI_MARKER}()`]
: []),
@@ -1792,7 +1798,7 @@ export class LlEmitter {
// The runtime stream vtables' intervals (streamVtStampLines) — the
// emitter story: instanceof and dynamic teardown dispatch through
// them.
- out.push(`@${iv.vt} = external global %ScrVt`);
+ out.push(`@${iv.vt} = external ${tl}global %ScrVt`);
stamps.push(
` store ${this.sizeType} ${iv.pre}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 0)`,
` store ${this.sizeType} ${iv.post}, ptr getelementptr inbounds (%ScrVt, ptr @${iv.vt}, i64 0, i32 1) ; ${iv.lib}`,
@@ -1832,9 +1838,10 @@ export class LlEmitter {
return out;
}
out.push(
- `define i32 @${this.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
+ `define i32 @${this.mod.workers ? "sc_context_entry" : this.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
`entry:`,
...(this.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []),
+ ...(this.mod.workers ? [` call void @scr_runtime_workers_v7()`] : []),
` call void @scr_init()`,
...stamps,
// Event-surface programs (signal/exit listeners) fill the loop's
@@ -1973,6 +1980,26 @@ export class LlEmitter {
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address }`] : []),
``,
);
+ if (this.mod.workers) {
+ out.push(
+ `declare void @scr_context_cleanup()`,
+ `declare i32 @scr_worker_argc()`,
+ `declare ptr @scr_worker_argv()`,
+ `define i32 @sc_worker_entry(i32 %root) ${FN_ATTRS} {`,
+ `entry:`,
+ ` %argc = call i32 @scr_worker_argc()`,
+ ` %argv = call ptr @scr_worker_argv()`,
+ ` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`,
+ ` ret i32 %result`,
+ `}`,
+ `define i32 @main(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
+ `entry:`,
+ ` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`,
+ ` call void @scr_context_cleanup()`,
+ ` ret i32 %result`,
+ `}`,
+ );
+ }
out.push(...this.fieldAliasMetadata());
if (this.debug !== null) out.push(this.debug.render());
return out;
@@ -2400,7 +2427,7 @@ export class LlEmitter {
ret === "void" ? ` ${call}` : ` %r = ${call}`,
ret === "void" ? ` ret void` : ` ret ${ret} %r`,
`}`,
- `@${mangleFnClosure(name)} = internal global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)} }`,
+ `@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)} }`,
``,
);
}
@@ -2962,6 +2989,20 @@ export class LlEmitter {
* using the result). Callers own the surrounding pending branch; a
* `throw` unwinds unconditionally. */
private emitUnwind(): void {
+ if (this.mod.workers && this.tryStack.length > 0) {
+ this.declare(`declare zeroext i1 @scr_context_stopping()`);
+ const stopped = this.B.tmp();
+ this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
+ const stop = this.B.newLabel("context.stop");
+ const ordinary = this.B.newLabel("context.throw");
+ this.B.condBr(stopped, stop, ordinary);
+ this.B.startBlock(stop);
+ const handlers = this.tryStack;
+ this.tryStack = [];
+ this.emitUnwind();
+ this.tryStack = handlers;
+ this.B.startBlock(ordinary);
+ }
const target = this.tryStack[this.tryStack.length - 1];
const frameDepth = target?.frameDepth ?? 0;
const scopeDepth = target?.scopeDepth ?? 0;
@@ -3023,6 +3064,37 @@ export class LlEmitter {
B.startBlock(lk);
}
+ private workerLoopBudget(): string | null {
+ if (!this.mod.workers) return null;
+ const slot = this.B.slot();
+ this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`);
+ this.B.line(`store i32 0, ptr ${slot}`);
+ return slot;
+ }
+
+ private emitWorkerLoopCheck(slot: string | null): void {
+ if (slot === null) return;
+ const B = this.B;
+ const budget = B.tmp(),
+ due = B.tmp(),
+ remaining = B.tmp();
+ const poll = B.newLabel("worker.poll"),
+ next = B.newLabel("worker.next");
+ B.line(`${budget} = load i32, ptr ${slot}`);
+ B.line(`${remaining} = sub i32 ${budget}, 1`);
+ B.line(`store i32 ${remaining}, ptr ${slot}`);
+ B.line(`${due} = icmp eq i32 ${budget}, 0`);
+ B.condBr(due, poll, next);
+ B.startBlock(poll);
+ B.line(`store i32 63, ptr ${slot}`);
+ // Calls and throwing operations retain their own checks. Amortize the
+ // extra cancellation poll across bounded loop batches so scalar work
+ // does not cross the runtime ABI on every iteration.
+ this.emitPendingCheck();
+ B.br(next);
+ B.startBlock(next);
+ }
+
/** Moves an already-evaluated value into the runtime's exception cell —
* the `throw` statement's kind dispatch (stmts.ts's), shared with
* every synthetic thrower. Ownership of a refcounted payload must have
@@ -3999,6 +4071,7 @@ export class LlEmitter {
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
}
+ if (this.mod.workers) this.emitPendingCheck();
this.emitStmts(fn.body);
// Implicit exit of a void function: release the function scope unless
// the body already terminated its final block (return, or a throw
@@ -4612,11 +4685,13 @@ export class LlEmitter {
break;
}
case "while": {
+ const workerBudget = this.workerLoopBudget();
const lc = B.newLabel("loop.c");
const lb = B.newLabel("loop.b");
const le = B.newLabel("loop.e");
B.br(lc);
B.startBlock(lc);
+ this.emitWorkerLoopCheck(workerBudget);
B.condBr(this.emitCondition(s.cond), lb, le);
B.startBlock(lb);
this.jumpTargets.push({
@@ -4635,12 +4710,14 @@ export class LlEmitter {
break;
}
case "doWhile": {
+ const workerBudget = this.workerLoopBudget();
// Body first (runs at least once); continue jumps to the CONDITION.
const lb = B.newLabel("loop.b");
const lc = B.newLabel("loop.c");
const le = B.newLabel("loop.e");
B.br(lb);
B.startBlock(lb);
+ this.emitWorkerLoopCheck(workerBudget);
this.jumpTargets.push({
kind: "loop",
brkLabel: le,
@@ -4802,8 +4879,10 @@ export class LlEmitter {
};
const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc;
freshenBindings();
+ const workerBudget = this.workerLoopBudget();
B.br(lc);
B.startBlock(lc);
+ this.emitWorkerLoopCheck(workerBudget);
if (integerLoop && integerSlot) {
const receiver = this.emitStableReceiver(integerLoop.limitReceiver, []);
const lenPtr = B.tmp();
@@ -4903,8 +4982,10 @@ export class LlEmitter {
const lb = B.newLabel("fof.b");
const lu = B.newLabel("fof.u");
const le = B.newLabel("fof.e");
+ const workerBudget = this.workerLoopBudget();
B.br(lc);
B.startBlock(lc);
+ this.emitWorkerLoopCheck(workerBudget);
const inBounds = B.tmp();
let cur: string;
if (snapshot && cursor && scratch) {
diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts
index 9d7fcc1c4..0d207eb95 100644
--- a/packages/compiler/src/backend/llvm/expr-bytes.ts
+++ b/packages/compiler/src/backend/llvm/expr-bytes.ts
@@ -189,6 +189,12 @@ export function emitBytesGet(
inBounds = false,
): LlValue {
const B = host.B;
+ if (host.mod.workers) {
+ host.declare(`declare double @scr_bytes_get(ptr, double)`);
+ const result = B.tmp();
+ B.line(`${result} = call double @scr_bytes_get(ptr ${receiver}, double ${index.name})`);
+ return { name: result, type: F64 };
+ }
const idx = host.emitBytesIndex(receiver, index, expr, inBounds);
const data = host.emitBytesData(receiver);
const p = B.tmp();
@@ -317,6 +323,11 @@ export function emitBytesSet(
inBounds = false,
): void {
const B = host.B;
+ if (host.mod.workers) {
+ host.declare(`declare void @scr_bytes_set(ptr, double, double)`);
+ B.line(`call void @scr_bytes_set(ptr ${receiver}, double ${index.name}, double ${value.name})`);
+ return;
+ }
// Invalid integer-indexed writes are ignored after evaluating the RHS.
const done = B.newLabel("bytes.store.done");
const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds);
diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts
index 70832eccd..976d0309b 100644
--- a/packages/compiler/src/backend/llvm/expr-calls.ts
+++ b/packages/compiler/src/backend/llvm/expr-calls.ts
@@ -459,6 +459,11 @@ export function emitCallExpr(
}
case "bytes":
case "mutable-bytes": {
+ if (host.mod.workers) {
+ host.declare(`declare void @scr_bytes_require_unshared(ptr)`);
+ B.line(`call void @scr_bytes_require_unshared(ptr ${arg.name})`);
+ host.emitPendingCheck();
+ }
const lenPtr = B.tmp();
const len = B.tmp();
const dataPtr = B.tmp();
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts
index dab517dde..878fae3d8 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts
@@ -17,7 +17,7 @@ function fixture(): IrModule {
loc,
});
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "__main",
functions: [{ name: "__main", params: [], returnType: VOID, locals: [], body, loc }],
diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts
index fadab5a2c..9f46ac3f3 100644
--- a/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts
+++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.test.ts
@@ -20,7 +20,7 @@ function sharedGraph(roots: number): IrModule {
unionId: `input${index}`,
}));
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
records: [
diff --git a/packages/compiler/src/backend/llvm/layouts.ts b/packages/compiler/src/backend/llvm/layouts.ts
index 41b94005d..6f03d626b 100644
--- a/packages/compiler/src/backend/llvm/layouts.ts
+++ b/packages/compiler/src/backend/llvm/layouts.ts
@@ -21,6 +21,13 @@ export function emitLlvmLayouts(
): LlvmLayouts {
const records = emitRecordShapes(host, mod);
const classShapes = emitClassShapes(host, mod, classes);
- const objects = emitClassObjDefs(host, classes, classObjects, functions, llType);
+ const objects = emitClassObjDefs(
+ host,
+ classes,
+ classObjects,
+ functions,
+ llType,
+ mod.workers === true || mod.lib?.threadInstances === true,
+ );
return { records, classes: classShapes, classObjects: objects };
}
diff --git a/packages/compiler/src/backend/llvm/lib-abi.ts b/packages/compiler/src/backend/llvm/lib-abi.ts
index 783e4cb12..f5023e1d5 100644
--- a/packages/compiler/src/backend/llvm/lib-abi.ts
+++ b/packages/compiler/src/backend/llvm/lib-abi.ts
@@ -47,6 +47,13 @@ export function finishRuntimeCall(
/** Strings inline their bytes; Buffer views carry a separate data pointer. */
export function rawBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[] {
const B = host.B;
+ if (host.mod.workers && value.type.kind === "bytes") {
+ // Native consumers can block or invoke callbacks. Keep a local copy of
+ // shared storage alive in the ownership frame instead of escaping its
+ // raw pointer or holding the shared-memory lock across the call.
+ const name = callRuntime(host, "scr_bytes_local_copy", "ptr", [ptr(value.name)]);
+ value = host.own({ name, type: value.type });
+ }
const lengthPtr = B.tmp(),
length = B.tmp(),
data = B.tmp();
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index ec83752de..4bc0f7a1d 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -12,6 +12,7 @@ import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
import { LIB_FN_SYMS, USES_TIMERS_LIB_FNS } from "./lib-shared.js";
import { DYN_KIND } from "./dyn.js";
+import { rawBytes } from "./lib-abi.js";
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
@@ -234,32 +235,10 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
: "scr_http_request";
let callArgs = head.map((a) => `${host.llType(a.type)} ${a.name}`);
if (isTlsOptions) {
- const ca = args[8]!;
- const caLenPtr = B.tmp();
- const caLen = B.tmp();
- let caData: string;
- if (ca.type.kind === "string") {
- caData = B.tmp();
- B.line(`${caLenPtr} = getelementptr inbounds %ScrStr, ptr ${ca.name}, i64 0, i32 1`);
- B.line(`${caLen} = load ${host.sizeType}, ptr ${caLenPtr}`);
- B.line(
- `${caData} = getelementptr inbounds i8, ptr ${ca.name}, i64 ${host.abiOffset(24, 12)}`,
- );
- } else if (ca.type.kind === "bytes" && ca.type.elem === "u8") {
- const caDataPtr = B.tmp();
- caData = B.tmp();
- B.line(
- `${caLenPtr} = getelementptr inbounds i8, ptr ${ca.name}, i64 ${host.abiOffset(8, 4)}`,
- );
- B.line(`${caLen} = load ${host.sizeType}, ptr ${caLenPtr}`);
- B.line(
- `${caDataPtr} = getelementptr inbounds i8, ptr ${ca.name}, i64 ${host.abiOffset(24, 12)}`,
- );
- B.line(`${caData} = load ptr, ptr ${caDataPtr}`);
- } else {
- throw new InternalCompilerError(`llvm emitter bug: ${e.fn} CA is not a string or Buffer`);
- }
- callArgs = [...callArgs.slice(0, 8), `ptr ${caData}`, `${host.sizeType} ${caLen}`];
+ callArgs = [
+ ...callArgs.slice(0, 8),
+ ...rawBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
+ ];
host.declare(
`declare ptr @scr_https_request(ptr, double, ptr, ptr, double, ptr, i1 zeroext, i1 zeroext, ptr, ${host.sizeType}, ptr, ptr)`,
);
@@ -364,6 +343,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "stdin":
return host.emitProcessLibCall(e);
case "module":
+ case "worker":
return host.emitGenericLibCall(e);
case "error":
case "regex":
@@ -394,6 +374,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "weakMap":
case "weakSet":
case "ffi":
+ case "sharedArrayBuffer":
case "arrayBuffer":
case "util":
case "bigint":
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 254eae579..621871a7b 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -37,6 +37,8 @@ export const LIB_FN_SYMS: Record = {
"bytes.construct": "scr_bytes_construct",
"bytes.instanceOf": "scr_bytes_instanceof",
"arrayBuffer.constructor": "scr_array_buffer_constructor",
+ "sharedArrayBuffer.new": "scr_shared_array_buffer_new",
+ "sharedArrayBuffer.is": "scr_shared_array_buffer_is",
"arrayBuffer.new": "scr_array_buffer_new",
"ffi.argument": "scr_ffi_argument",
"ffi.memoryModule": "scr_ffi_memory_module",
@@ -716,7 +718,15 @@ export const LIB_FN_SYMS: Record = {
"fs.chownSync": "scr_fs_chown",
"fs.mkdirModeSync": "scr_fs_mkdir_mode",
"fs.mkdirRecursiveModeSync": "scr_fs_mkdir_recursive_mode",
+ "atomics.op": "scr_atomics_op",
+ "atomics.notify": "scr_atomics_notify",
"atomics.wait": "scr_atomics_wait",
+ "worker.new": "scr_worker_new",
+ "worker.isMainThread": "scr_context_is_main",
+ "worker.threadId": "scr_context_thread_number",
+ "worker.root": "scr_worker_root",
+ "worker.data": "scr_worker_data",
+ "worker.parentPort": "scr_worker_parent_port",
"process.stdinDestroy": "scr_process_stdin_destroy",
"process.stdinSetRawMode": "scr_process_stdin_set_raw_mode",
// node:events EventEmitter (scr_events_emitter.c): receivers borrowed,
@@ -1060,6 +1070,8 @@ export const LIB_FN_SYMS: Record = {
};
export const USES_TIMERS_LIB_FNS = new Set([
+ "worker.new",
+ "worker.parentPort",
"process.stdio",
"fetch.start",
"fetch.input",
diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts
index 4178293d9..ef6778281 100644
--- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts
+++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts
@@ -124,7 +124,7 @@ function fixture(): IrModule {
],
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
records: [{ id: "cell", fields: [{ name: "x", type: F64 }] }],
diff --git a/packages/compiler/src/backend/llvm/local-union-storage.test.ts b/packages/compiler/src/backend/llvm/local-union-storage.test.ts
index 30916af16..63f88eefa 100644
--- a/packages/compiler/src/backend/llvm/local-union-storage.test.ts
+++ b/packages/compiler/src/backend/llvm/local-union-storage.test.ts
@@ -81,7 +81,7 @@ function fixture(): IrModule {
],
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
unions: [{ id: "optional", arms: [STRING, UNDEFINED_T] }],
diff --git a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts
index 9e29cdc24..dc8d91ff5 100644
--- a/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts
+++ b/packages/compiler/src/backend/llvm/map-read-lifetimes.test.ts
@@ -66,7 +66,7 @@ function fixture(value: IrType = record, key: IrType = F64): IrModule {
],
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
records: [{ id: "cell", fields: [{ name: "x", type: F64 }] }],
diff --git a/packages/compiler/src/backend/llvm/split-thread-state.test.c b/packages/compiler/src/backend/llvm/split-thread-state.test.c
new file mode 100644
index 000000000..fd89e7998
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/split-thread-state.test.c
@@ -0,0 +1,39 @@
+#undef NDEBUG
+#include
+#include
+#include
+#include
+#include
+#include
+
+_Thread_local int64_t foreign = 13;
+extern void set_owner(int64_t owner);
+extern int64_t get_owner(void);
+extern int64_t next_value(void);
+
+enum { THREADS = 4, ITERATIONS = 10000 };
+static atomic_uint ready;
+
+static void *run(void *argument) {
+ int64_t id = (int64_t)(uintptr_t)argument;
+ assert(get_owner() == 0);
+ set_owner(id);
+ atomic_fetch_add_explicit(&ready, 1, memory_order_release);
+ while (atomic_load_explicit(&ready, memory_order_acquire) != THREADS) sched_yield();
+ for (int64_t i = 0; i < ITERATIONS; i++) {
+ assert(get_owner() == id);
+ assert(next_value() == 20 + 2 * i);
+ if (i % 64 == 0) sched_yield();
+ }
+ assert(foreign == 13 + ITERATIONS);
+ return NULL;
+}
+
+int main(void) {
+ pthread_t threads[THREADS];
+ for (uintptr_t i = 0; i < THREADS; i++)
+ assert(pthread_create(&threads[i], NULL, run, (void *)(i + 1)) == 0);
+ for (unsigned i = 0; i < THREADS; i++) assert(pthread_join(threads[i], NULL) == 0);
+ assert(get_owner() == 0 && next_value() == 20 && foreign == 14);
+ puts("thread state stays isolated across compiled partitions");
+}
diff --git a/packages/compiler/src/backend/llvm/split.test.ts b/packages/compiler/src/backend/llvm/split.test.ts
index 6219f6748..a49a11545 100644
--- a/packages/compiler/src/backend/llvm/split.test.ts
+++ b/packages/compiler/src/backend/llvm/split.test.ts
@@ -76,14 +76,84 @@ test("splits generated LLVM functions and promotes only private cross-shard defi
).toBe(true);
});
-test("thread-local program state conservatively keeps the single-TU path", () => {
+test("thread-local definitions and declarations preserve their storage model across shards", () => {
const tls = SAMPLE.replace(
"@hidden_value = internal global i64 7",
"@hidden_value = internal thread_local global i64 7",
);
- expect(splitLlvmProgram(tls, { minimumBytes: 0, targetBytes: 64 * 1024 })).toBeNull();
+ const split = splitLlvmProgram(tls, { minimumBytes: 0, targetBytes: 64 * 1024 })!;
+ expect(split.shards[0]!.source).toContain("@hidden_value = hidden thread_local global i64 7");
+ for (const shard of split.shards.slice(1)) {
+ expect(shard.source).toContain("@hidden_value = external hidden thread_local global i64");
+ }
});
+test.skipIf(process.platform === "win32")(
+ "split and merged native modules keep globals independent on concurrent threads",
+ async () => {
+ const source = `%State = type { i64, i64 }
+@state = internal thread_local global %State { i64 7, i64 0 }
+@foreign = external thread_local global i64
+
+define void @set_owner(i64 %owner) {
+entry:
+ store i64 %owner, ptr getelementptr inbounds (%State, ptr @state, i64 0, i32 1)
+ ret void
+}
+
+define i64 @get_owner() {
+entry:
+ %owner = load i64, ptr getelementptr inbounds (%State, ptr @state, i64 0, i32 1)
+ ret i64 %owner
+}
+
+define i64 @next_value() {
+entry:
+ %current = load i64, ptr @state
+ %next = add i64 %current, 1
+ store i64 %next, ptr @state
+ %other = load i64, ptr @foreign
+ %after = add i64 %other, 1
+ store i64 %after, ptr @foreign
+ %result = add i64 %current, %other
+ ret i64 %result
+}
+`;
+ const split = splitLlvmProgram(source, { minimumBytes: 0, targetBytes: 64 * 1024 })!;
+ expect(split.shards.length).toBeGreaterThan(2);
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-thread-state-"));
+ scratch.push(dir);
+ const objects: string[] = [];
+ for (const shard of split.shards) {
+ const path = join(dir, shard.name);
+ const object = `${path}.o`;
+ await writeFile(path, shard.source);
+ execFileSync("clang", ["-Wno-override-module", "-O2", "-c", path, "-o", object]);
+ objects.push(object);
+ }
+ const combined = join(dir, "combined.o");
+ execFileSync("ld", ["-r", ...objects, "-o", combined]);
+ if (process.platform === "linux") {
+ const keep = join(dir, "keep.txt");
+ await writeFile(keep, "set_owner\nget_owner\nnext_value\n");
+ execFileSync("objcopy", [`--keep-global-symbols=${keep}`, combined]);
+ }
+ const binary = join(dir, "thread-state");
+ execFileSync("clang", [
+ "-std=c11",
+ "-O2",
+ "-pthread",
+ join(import.meta.dirname, "split-thread-state.test.c"),
+ combined,
+ "-o",
+ binary,
+ ]);
+ expect(execFileSync(binary, { encoding: "utf8", timeout: 10000 })).toBe(
+ "thread state stays isolated across compiled partitions\n",
+ );
+ },
+);
+
test("shared metadata bounds shard growth while preserving every definition", () => {
const names = Array.from({ length: 120 }, (_, i) => `metadata_pad_${i}`);
const body = names
diff --git a/packages/compiler/src/backend/llvm/split.ts b/packages/compiler/src/backend/llvm/split.ts
index b77aecd6a..3fb41ce6b 100644
--- a/packages/compiler/src/backend/llvm/split.ts
+++ b/packages/compiler/src/backend/llvm/split.ts
@@ -202,12 +202,6 @@ export function splitLlvmProgram(
if (/^@.+\s+=\s+external\s+/.test(line)) preamble.push(line);
else return null;
} else {
- // LLVM textual external TLS declarations compile on Darwin, but GNU
- // ELF clang emits the cross-shard reference as non-TLS and `ld -r`
- // correctly refuses the mismatch. Thread-instanced libraries keep
- // the canonical single-TU path until the producer can preserve TLS
- // model metadata portably across object formats.
- if (/\bthread_local(?:\([^)]*\))?\s+global\b/.test(line)) return null;
globals.push(parsed);
sawDefinition = true;
}
diff --git a/packages/compiler/src/backend/llvm/stack-unions.test.ts b/packages/compiler/src/backend/llvm/stack-unions.test.ts
index 189a963a8..1f61b655b 100644
--- a/packages/compiler/src/backend/llvm/stack-unions.test.ts
+++ b/packages/compiler/src/backend/llvm/stack-unions.test.ts
@@ -69,7 +69,7 @@ function fixture(arm: IrType = record): IrModule {
body: [ret(narrow(ref("value"), arm))],
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
records: [{ id: "cell", fields: [{ name: "x", type: F64 }] }],
diff --git a/packages/compiler/src/backend/llvm/string-construction.test.ts b/packages/compiler/src/backend/llvm/string-construction.test.ts
index 2f8dbd7ac..160dd768f 100644
--- a/packages/compiler/src/backend/llvm/string-construction.test.ts
+++ b/packages/compiler/src/backend/llvm/string-construction.test.ts
@@ -17,7 +17,7 @@ const concat = (left: IrExpr, right: IrExpr): IrExpr => ({
function module(value: IrExpr): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [
diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts
index ff192dc12..b1d667659 100644
--- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts
+++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts
@@ -68,7 +68,7 @@ function fn(name: string, names: string[], value: IrExpr): IrFunction {
}
function mod(...functions: IrFunction[]): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [
diff --git a/packages/compiler/src/backend/llvm/switch-dispatch.test.ts b/packages/compiler/src/backend/llvm/switch-dispatch.test.ts
index ae11b63a9..f40a02d5a 100644
--- a/packages/compiler/src/backend/llvm/switch-dispatch.test.ts
+++ b/packages/compiler/src/backend/llvm/switch-dispatch.test.ts
@@ -8,7 +8,7 @@ const string = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING
function emit(type: IrType, tests: (IrExpr | null)[], pointerBits: 32 | 64 = 64): string {
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [
diff --git a/packages/compiler/src/backend/may-throw.test.ts b/packages/compiler/src/backend/may-throw.test.ts
index 535046404..899ed300e 100644
--- a/packages/compiler/src/backend/may-throw.test.ts
+++ b/packages/compiler/src/backend/may-throw.test.ts
@@ -26,7 +26,7 @@ function fn(name: string, body: IrStmt[], locals: IrLocal[] = [local]): IrFuncti
return { name, body, locals, params: [], returnType: VOID, loc };
}
function moduleWith(...functions: IrFunction[]): IrModule {
- return { irVersion: 13, sourceFile: loc.file, entry: "caller", functions };
+ return { irVersion: 14, sourceFile: loc.file, entry: "caller", functions };
}
const assignment: IrStmt = { kind: "assign", localId: local.id, value, loc };
const expression: IrExpr = { kind: "assignExpr", localId: local.id, value, type: F64, loc };
diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts
index c47ab7e86..6a419b78b 100644
--- a/packages/compiler/src/backend/may-throw.ts
+++ b/packages/compiler/src/backend/may-throw.ts
@@ -207,7 +207,8 @@ export function computeMayThrow(mod: IrModule): { fns: Set; indirect: bo
// A native callback may run arbitrary scriptc code. With retained
// descriptors any manifest binding may pump a previously stored
// callback, so every FFI call is conservatively a checkpoint.
- if (callbackFfiImports.has(rec.import) || manifestHasRetainedCallback) f.throws = true;
+ if (mod.workers || callbackFfiImports.has(rec.import) || manifestHasRetainedCallback)
+ f.throws = true;
break;
case "bytesNew": {
// The size form (`new Uint8Array(n)`) throws Node's "Invalid
diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts
index b9fa8349e..4f7ea46b6 100644
--- a/packages/compiler/src/backend/native-link-info-core.ts
+++ b/packages/compiler/src/backend/native-link-info-core.ts
@@ -14,6 +14,7 @@ import type {
import type { NativeTargetSpec } from "./targets.js";
export interface NativeLinkFeatures {
+ workers?: boolean;
dynamic: boolean;
regex: boolean;
copying: boolean;
diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts
index b99536a6c..26f59761c 100644
--- a/packages/compiler/src/backend/native/contracts.ts
+++ b/packages/compiler/src/backend/native/contracts.ts
@@ -48,6 +48,8 @@ export interface CcOptions {
* static runtime selection; executable section GC may still remove
* unreachable static-runtime code. */
dynamic?: boolean;
+ /** Isolated native Worker contexts; every runtime object must use TLS. */
+ workers?: boolean;
/** The program contains a regex construct (index.ts detects it on the
* IR): compiles scr_regex.c and links the vendored libregexp — as cached
* standalone objects in static builds, from the engine archive under
diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts
index 6b9291132..00658b42a 100644
--- a/packages/compiler/src/backend/native/executable.ts
+++ b/packages/compiler/src/backend/native/executable.ts
@@ -127,6 +127,8 @@ export async function compileCInternal(
const sanitize = opts.sanitize ?? false;
const optimization = opts.optimization ?? "release";
const dynamic = opts.dynamic ?? false;
+ if (opts.workers && (dynamic || targetPlatform(resolveCc()) === "wasi"))
+ throw new Error("worker threads require a native static executable");
const regex = opts.regex ?? false;
// fetch's implementation switch: the default is the NATIVE bridge
// (scr_fetch.c over scr_net + scr_tls + scr_http's client parser +
@@ -547,6 +549,7 @@ export async function compileCInternal(
...debugFlags,
...driver.targetArgs,
...threadArgs,
+ ...(opts.workers ? ["-DSCR_WORKERS"] : []),
...(sanitize
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
: [optimization === "dev" ? "-O0" : "-O2"]),
@@ -569,6 +572,11 @@ export async function compileCInternal(
"-I",
rtDir,
...runtimeSources.map((f) => rt(join(rtDir, f))),
+ ...(opts.workers
+ ? ["scr_worker.c", "scr_worker_events.c", "scr_mailbox.c", "scr_message.c"].map((f) =>
+ rt(join(rtDir, f)),
+ )
+ : []),
...(opts.copying ? [rt(join(rtDir, "scr_copying.c"))] : []),
...(opts.fileHandle ? [rt(join(rtDir, "scr_file_handle.c"))] : []),
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
@@ -589,9 +597,9 @@ export async function compileCInternal(
...(opts.inspect
? [rt(join(rtDir, "scr_inspect.c")), rt(join(rtDir, "scr_console_native.c"))]
: []),
- ...(opts.dynInvoke || nativeFetch ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
+ ...(opts.dynInvoke || opts.workers || nativeFetch ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []),
- ...(opts.dynAsync || opts.dynInvoke || opts.dc || opts.fileHandle || nativeFetch
+ ...(opts.dynAsync || opts.dynInvoke || opts.workers || opts.dc || opts.fileHandle || nativeFetch
? [rt(join(rtDir, "scr_async_dyn.c"))]
: []),
// The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
@@ -611,12 +619,12 @@ export async function compileCInternal(
// installer exactly then.
...(opts.zlib && opts.dynamic ? [rt(join(rtDir, "scr_zlib_island.c"))] : []),
...(opts.events ? [rt(join(rtDir, "scr_events.c")), rt(join(rtDir, "scr_readline.c"))] : []),
- ...(opts.emitter ? [rt(join(rtDir, "scr_events_emitter.c"))] : []),
+ ...(opts.emitter || opts.workers ? [rt(join(rtDir, "scr_events_emitter.c"))] : []),
// The checked-dynamic HANDLE support unit (listener gate + runtime
// adapter closures): every referencing unit is one of the emitter or
// net families (http implies net), so handle-free binaries keep
// their exact size class.
- ...(opts.emitter || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []),
+ ...(opts.emitter || opts.workers || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []),
...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []),
...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []),
...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []),
@@ -753,6 +761,7 @@ export async function compileCInternal(
...debugFlags,
...driver.targetArgs,
...threadArgs,
+ ...(opts.workers ? ["-DSCR_WORKERS"] : []),
...(sanitize
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
: [optimization === "dev" ? "-O0" : "-O2"]),
@@ -974,6 +983,7 @@ export async function compileCInternal(
const linkProbeArgs = [
...(sanitize ? ["-fsanitize=address"] : []),
...threadArgs,
+ ...(opts.workers ? ["-DSCR_WORKERS"] : []),
...(targetPlatform(driver) === "win32" ? ["-ladvapi32", "-liphlpapi", "-lws2_32"] : []),
...(tls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts
index 46e602638..5d134cfba 100644
--- a/packages/compiler/src/backend/native/runtime-inputs.ts
+++ b/packages/compiler/src/backend/native/runtime-inputs.ts
@@ -37,6 +37,8 @@ export const NATIVE_RECIPE_IMPLEMENTATION_PATHS = [
].map((stem) => join(backendDir, `${stem}${extname(implementationPath)}`));
export const EXECUTABLE_RUNTIME_SOURCES = [
+ "scr_context.c",
+ "scr_shared.c",
"scr_number.c",
"scr_bigint.c",
"scr_string.c",
diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts
index 02dbd2c2d..739a86cb1 100644
--- a/packages/compiler/src/backend/runtime-abi.ts
+++ b/packages/compiler/src/backend/runtime-abi.ts
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
-export const RUNTIME_ABI_VERSION = 6 as const;
-export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v6" as const;
+export const RUNTIME_ABI_VERSION = 7 as const;
+export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v7" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts
index d75d9ba0f..8ed57beb0 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -75,6 +75,7 @@ export interface RuntimePackManifest {
}
export interface RuntimeFeatureSet extends NativeLinkFeatures {
+ workers: boolean;
nativeFetch: boolean;
netIslandEffective: boolean;
netEffective: boolean;
@@ -263,6 +264,7 @@ export function effectiveRuntimeFeatures(
const tlsCaEffective = features.tlsCa || tlsEffective;
return {
...features,
+ workers: features.workers === true,
nativeFetch,
netIslandEffective,
netEffective,
@@ -297,6 +299,12 @@ function selectVariant(unit: RuntimePackUnit, features: RuntimeFeatureSet): Runt
if (selected === undefined) {
throw new RuntimePackError(`runtime pack has no variant for ${unit.source}`, "invalid");
}
+ if ((features.workers === true) !== selected.defines.includes("SCR_WORKERS")) {
+ throw new RuntimePackError(
+ `runtime pack has an incompatible thread-state variant for ${unit.source}; reinstall the matching runtime package`,
+ "invalid",
+ );
+ }
return selected;
}
@@ -346,6 +354,11 @@ export function selectRuntimePackArtifacts(
mode: RuntimePackMode = "executable",
): RuntimePackArtifacts {
const features = effectiveRuntimeFeatures(requested, env);
+ if (
+ features.workers &&
+ (mode !== "executable" || features.dynamic || manifest.target.object_format === "wasm")
+ )
+ throw new RuntimePackError("worker threads require a native static executable", "unsupported");
const key: RuntimePackFlavor = mode === "executable" ? flavor : `${mode}-${flavor}`;
const selectedFlavor =
mode === "library"
diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts
index 2e03e4014..8e8598cad 100644
--- a/packages/compiler/src/backend/runtime-pack-targets.test.ts
+++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts
@@ -21,7 +21,7 @@ afterEach(async () =>
Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))),
);
-async function fixture(target: NativeTargetSpec) {
+async function fixture(target: NativeTargetSpec, workers = false) {
const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-target-"));
dirs.push(root);
const packagePath = join(root, "package.json");
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
- runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" },
+ runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -62,7 +62,9 @@ async function fixture(target: NativeTargetSpec) {
{
source: "scr_runtime.c",
predicate: true,
- variants: [{ id: "default", when: {}, defines: [], ...artifact }],
+ variants: [
+ { id: "default", when: {}, defines: workers ? ["SCR_WORKERS"] : [], ...artifact },
+ ],
},
],
},
@@ -72,7 +74,9 @@ async function fixture(target: NativeTargetSpec) {
{
source: "scr_runtime.c",
predicate: true,
- variants: [{ id: "default", when: {}, defines: [], ...artifact }],
+ variants: [
+ { id: "default", when: {}, defines: workers ? ["SCR_WORKERS"] : [], ...artifact },
+ ],
},
],
},
@@ -141,3 +145,46 @@ test.each([
object_format: target.objectFormat,
});
});
+
+test.each([
+ MACOS_X64_TARGET,
+ LINUX_X64_GNU_TARGET,
+ LINUX_ARM64_GNU_TARGET,
+ WINDOWS_X64_MSVC_TARGET,
+ LINUX_X64_MUSL_TARGET,
+ LINUX_ARM64_MUSL_TARGET,
+])("worker objects require matching thread-state variants for $name", async (target) => {
+ const ordinary = await fixture(target);
+ const worker = await fixture(target, true);
+ const request = {
+ target,
+ features: { ...features, workers: true },
+ optimization: "release" as const,
+ };
+ await expect(
+ loadRuntimePack({ ...request, resolver: () => ordinary.packagePath }),
+ ).rejects.toThrow("incompatible thread-state variant");
+ await expect(
+ loadRuntimePack({ ...request, resolver: () => worker.packagePath }),
+ ).resolves.toBeDefined();
+ await expect(
+ loadRuntimePack({ ...request, features, resolver: () => worker.packagePath }),
+ ).rejects.toThrow("incompatible thread-state variant");
+});
+
+test("worker runtime packs refuse dynamic and WASI execution", async () => {
+ for (const [target, dynamic] of [
+ [LINUX_X64_GNU_TARGET, true],
+ [WASM32_WASI_TARGET, false],
+ ] as const) {
+ const pack = await fixture(target, true);
+ await expect(
+ loadRuntimePack({
+ target,
+ features: { ...features, workers: true, dynamic },
+ optimization: "release",
+ resolver: () => pack.packagePath,
+ }),
+ ).rejects.toThrow("native static executable");
+ }
+});
diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts
index 0c363947f..986bb0655 100644
--- a/packages/compiler/src/backend/runtime-pack.test.ts
+++ b/packages/compiler/src/backend/runtime-pack.test.ts
@@ -112,7 +112,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
- runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" },
+ runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" },
compiler: {
command: "clang",
identity: "fixture clang",
diff --git a/packages/compiler/src/backend/target-diagnostics.test.ts b/packages/compiler/src/backend/target-diagnostics.test.ts
index 8bab58731..c4c25e270 100644
--- a/packages/compiler/src/backend/target-diagnostics.test.ts
+++ b/packages/compiler/src/backend/target-diagnostics.test.ts
@@ -35,7 +35,7 @@ const fetch: IrExpr = {
};
function module(direct: IrExpr[] = []): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "%main",
records: [],
diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts
index 0c04bf922..197a11dc4 100644
--- a/packages/compiler/src/executable/executable-cache.test.ts
+++ b/packages/compiler/src/executable/executable-cache.test.ts
@@ -161,6 +161,22 @@ test("early dev cache restores the matching dSYM and rejects a missing payload",
expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull();
});
+test("early executable cache preserves worker runtime selection when only frontend output restores", async () => {
+ const f = await fixture();
+ const tracker = new FrontendInputTracker();
+ tracker.run(() => trackedReadFile(f.source));
+ await publishEarlyExecutableCache(f.root, f.options, {
+ llvmPath: f.llvmPath,
+ irPath: f.irPath,
+ native: { ...native, workers: true },
+ executableRestored: false,
+ frontend: tracker.snapshot(),
+ });
+ const restored = await readEarlyExecutableCache(f.root, f.options);
+ expect(restored?.native.workers).toBe(true);
+ expect(restored?.executableRestored).toBe(false);
+});
+
test("early executable cache misses on source and resolution changes", async () => {
const f = await fixture();
const tracker = new FrontendInputTracker();
diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts
index ea7aaa06b..2f093e3b0 100644
--- a/packages/compiler/src/executable/executable-cache.ts
+++ b/packages/compiler/src/executable/executable-cache.ts
@@ -48,6 +48,7 @@ export interface EarlyExecutableNativeFeatures {
/** Omitted is the historical release posture. */
optimization?: "dev";
dynamic: boolean;
+ workers?: boolean;
regex: boolean;
copying: boolean;
textDecoderLegacy: boolean;
@@ -194,7 +195,9 @@ function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeat
return validSharedNativeFeatures(
value,
BOOLEAN_NATIVE_KEYS,
- (native) => native.optimization === undefined || native.optimization === "dev",
+ (native) =>
+ (native.optimization === undefined || native.optimization === "dev") &&
+ (native.workers === undefined || typeof native.workers === "boolean"),
);
}
diff --git a/packages/compiler/src/frontend/fork-target.test.ts b/packages/compiler/src/frontend/fork-target.test.ts
index d2f6c70f6..7a3e23c9b 100644
--- a/packages/compiler/src/frontend/fork-target.test.ts
+++ b/packages/compiler/src/frontend/fork-target.test.ts
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test, vi } from "vitest";
import { forkTargetPaths } from "./fork-target.js";
+import { fallbackDtsPath } from "./dts-paths.js";
import { Ts7Host } from "./ts7/program-host.js";
import { Ts7Api } from "./ts7/rpc-api.js";
import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js";
@@ -10,11 +11,7 @@ import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js";
test("fork discovery batches only candidate bindings and preserves aliases and shadowing", () => {
const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-discovery-"));
const entry = join(directory, "main.ts");
- const declarations = join(directory, "builtins.d.ts");
- writeFileSync(
- declarations,
- 'declare module "node:child_process" { export function fork(path: unknown): void; }',
- );
+ const declarations = fallbackDtsPath();
writeFileSync(
join(directory, "facade.ts"),
'export { fork as launch } from "node:child_process";',
diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts
index 3ec3eb28c..0562467be 100644
--- a/packages/compiler/src/frontend/fork-target.ts
+++ b/packages/compiler/src/frontend/fork-target.ts
@@ -1,9 +1,26 @@
import { resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import * as ts from "./ts7/adapter.js";
+import { fallbackDtsPath, isNodeTypesPath, tsgoPath } from "./dts-paths.js";
import { canonicalBuiltinModule } from "./builtin-modules.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
+export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): boolean {
+ const checker = program.getTypeChecker();
+ const symbol = checker.getSymbolAtLocation(expression);
+ const declarations = symbol ? checker.declarationsOf(symbol) : [];
+ return (
+ declarations.length > 0 &&
+ declarations.every((declaration) => {
+ const source = declaration.getSourceFile();
+ return (
+ source.isDeclarationFile &&
+ (tsgoPath(source.fileName) === fallbackDtsPath() || isNodeTypesPath(source.fileName))
+ );
+ })
+ );
+}
+
function strip(expr: ts.Expression): ts.Expression {
let current = expr;
while (
@@ -64,7 +81,7 @@ export function isBuiltinMemberImport(
/** True when an identifier is the namespace/default binding for one Node
* builtin module. JavaScript default imports of CommonJS builtins expose the
* same module object as namespace imports, matching lowering's provenance. */
-function isBuiltinNamespaceImport(
+export function isBuiltinNamespaceImport(
program: ts.Program,
ident: ts.Identifier,
moduleName: string,
@@ -217,7 +234,8 @@ function staticString(
if (
ts.isNewExpression(current) &&
ts.isIdentifier(current.expression) &&
- current.expression.text === "URL"
+ (isBuiltinMemberImport(program, current.expression, "url", "URL") ||
+ (current.expression.text === "URL" && isNodeGlobal(program, current.expression)))
) {
const args = current.arguments ?? [];
if (args.length < 1 || args.length > 2) return null;
diff --git a/packages/compiler/src/frontend/lowering/builtins/atomics.ts b/packages/compiler/src/frontend/lowering/builtins/atomics.ts
index 3f5317204..b4a394dce 100644
--- a/packages/compiler/src/frontend/lowering/builtins/atomics.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/atomics.ts
@@ -2,18 +2,22 @@ import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { F64, type IrExpr, STRING } from "../../../ir/ir.js";
+import { lowerOptionalArgument } from "../optional-arguments.js";
-/** `Atomics.wait(int32Array, idx, expected, timeoutMs)` — the
- * synchronous-sleep idiom (RouteStore's
- * `Atomics.wait(sleepBuffer, 0, 0, ms)`): scriptc has no threads, so
- * nothing can ever notify a waiter and the spec's behavior for every
- * compilable program is exactly "compare, then sleep out the timeout"
- * — "not-equal" when the element differs, a real nanosleep and
- * "timed-out" otherwise ("ok" is unreachable). The timeout argument is
- * REQUIRED: without it Node blocks until a notify that cannot exist
- * here — a certain deadlock, fenced with that explanation. Every other
- * Atomics member (notify has no one to wake; add/load/... — nothing
- * races) fences member-qualified. Null for non-Atomics receivers. */
+const OPERATIONS = [
+ "load",
+ "store",
+ "exchange",
+ "compareExchange",
+ "add",
+ "sub",
+ "and",
+ "or",
+ "xor",
+];
+
+/** Familiar integer Atomics over fixed typed storage. Wait queues are keyed
+ * by backing allocation and byte offset, so distinct views address one queue. */
export function lowerAtomicsCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -22,41 +26,80 @@ export function lowerAtomicsCall(
const member = lowerer.stdlibGlobalMember(access, "Atomics");
if (member === null) return null;
const loc = locOf(call);
- if (member !== "wait") {
+ const operation = OPERATIONS.indexOf(member);
+ if (operation < 0 && member !== "wait" && member !== "notify") {
lowerer.noLowering(
`Atomics.${member}`,
call,
- "Atomics.wait(int32Array, idx, expected, timeoutMs) is the supported Atomics surface " +
- "(scriptc has no threads — wait is the synchronous-sleep idiom, and nothing else has anyone to race)",
+ "use integer load, store, exchange, compareExchange, add, sub, and, or, xor, wait or notify",
lowerer.checker.getSymbolAtLocation(access.name),
);
}
- if (call.arguments.length !== 4 || call.arguments.some(ts.isSpreadElement)) {
+ const minimum =
+ member === "wait"
+ ? 3
+ : member === "notify" || member === "load"
+ ? 2
+ : member === "compareExchange"
+ ? 4
+ : 3;
+ const maximum = member === "wait" ? 4 : member === "notify" ? 3 : minimum;
+ if (
+ call.arguments.length < minimum ||
+ call.arguments.length > maximum ||
+ call.arguments.some(ts.isSpreadElement)
+ ) {
lowerer.noLowering(
- `Atomics.wait with ${call.arguments.length} arguments`,
+ `Atomics.${member} arguments`,
call,
- "the timeout is required: without it the wait blocks forever — scriptc has no threads, " +
- "so no notify can ever arrive (Atomics.wait(arr, idx, expected, timeoutMs))",
+ `use ${minimum}${maximum > minimum ? `–${maximum}` : ""} arguments`,
);
}
- const arrNode = call.arguments[0]!;
- const arrIr = lowerer.mapTypeOf(lowerer.typeOf(arrNode));
- if (!(arrIr?.kind === "bytes" && arrIr.elem === "i32")) {
+ const source = call.arguments[0]!;
+ // Generic constraints can remain unions in the checker while this body
+ // has a concrete typed-array binding. Validate the actual lowered storage.
+ const array = lowerer.lowerExpr(source);
+ const type = array.type;
+ if (
+ type?.kind !== "bytes" ||
+ !["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) ||
+ ((member === "wait" || member === "notify") && type.elem !== "i32")
+ ) {
lowerer.noLowering(
- `Atomics.wait over '${lowerer.checker.typeToString(lowerer.typeOf(arrNode))}' values`,
- arrNode,
- "an Int32Array is the supported waitable array",
+ `Atomics.${member} over this array`,
+ source,
+ member === "wait" || member === "notify"
+ ? "use an Int32Array"
+ : "use an 8-, 16-, or 32-bit integer typed array",
);
}
- const arr = lowerer.lowerExpr(arrNode);
- const idx = lowerer.lowerExprExpecting(call.arguments[1]!, F64);
- const expected = lowerer.lowerExprExpecting(call.arguments[2]!, F64);
- const timeout = lowerer.lowerExprExpecting(call.arguments[3]!, F64);
+ const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
+ const arg = (index: number, fallback: number): IrExpr =>
+ call.arguments[index]
+ ? lowerOptionalArgument(lowerer, call.arguments[index]!, F64, number(fallback))
+ : number(fallback);
+ const index = arg(1, 0);
+ if (member === "wait")
+ return {
+ kind: "libCall",
+ fn: "atomics.wait",
+ args: [array, index, arg(2, 0), arg(3, Infinity)],
+ type: STRING,
+ loc,
+ };
+ if (member === "notify")
+ return {
+ kind: "libCall",
+ fn: "atomics.notify",
+ args: [array, index, arg(2, Infinity)],
+ type: F64,
+ loc,
+ };
return {
kind: "libCall",
- fn: "atomics.wait",
- args: [arr, idx, expected, timeout],
- type: STRING,
+ fn: "atomics.op",
+ args: [array, index, arg(2, 0), arg(3, 0), number(operation)],
+ type: F64,
loc,
};
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts
new file mode 100644
index 000000000..66239d076
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts
@@ -0,0 +1,98 @@
+import * as ts from "../../ts7/adapter.js";
+import { locOf } from "../../program.js";
+import { resolve } from "node:path";
+import { isWorkerConstructor, workerModulePath } from "../../worker-target.js";
+import { tsgoPath } from "../../dts-paths.js";
+import {
+ BOOL,
+ DYN,
+ F64,
+ STRING,
+ UNDEFINED_T,
+ isFfiCallbackParam,
+ type IrExpr,
+ type SrcLoc,
+} from "../../../ir/ir.js";
+import type { Lowerer } from "../lowerer.js";
+
+export function lowerWorkerMetadata(member: string, loc: SrcLoc): IrExpr | null {
+ switch (member) {
+ case "isMainThread":
+ return { kind: "libCall", fn: "worker.isMainThread", args: [], type: BOOL, loc };
+ case "threadId":
+ return { kind: "libCall", fn: "worker.threadId", args: [], type: F64, loc };
+ case "workerData":
+ return { kind: "libCall", fn: "worker.data", args: [], type: DYN, loc };
+ case "parentPort":
+ return { kind: "libCall", fn: "worker.parentPort", args: [], type: DYN, loc };
+ case "isInternalThread":
+ return { kind: "boolLit", value: false, type: BOOL, loc };
+ default:
+ return null;
+ }
+}
+
+export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): IrExpr | null {
+ const constructor = expression.expression;
+ if (!isWorkerConstructor(lowerer.program, constructor)) return null;
+ const args = expression.arguments ?? [];
+ if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(
+ "Worker constructor arguments",
+ expression,
+ "use new Worker(filename, options?)",
+ );
+ }
+ const filename = args[0]!;
+ const path = workerModulePath(lowerer.program, filename);
+ const root =
+ path === null ? undefined : lowerer.workerTargetIdByPath.get(tsgoPath(resolve(path)));
+ if (root === undefined) {
+ lowerer.noLowering(
+ "this Worker entry point",
+ filename,
+ "the worker entry must resolve to a statically compiled source file",
+ );
+ }
+ if (lowerer.dynamic || lowerer.targetPlatform === "wasi") {
+ lowerer.noLowering(
+ "Worker execution in this target",
+ expression,
+ "use a native static executable",
+ );
+ }
+ if (
+ lowerer.ffiImports.some((entry) =>
+ entry.params.some(
+ (parameter) => isFfiCallbackParam(parameter) && parameter.callback.invoke === "foreign",
+ ),
+ )
+ ) {
+ lowerer.noLowering(
+ "workers with foreign-thread native callbacks",
+ expression,
+ "native callbacks in worker executables must run on their owning script thread",
+ );
+ }
+ lowerer.usesWorkers = true;
+ const loc = locOf(expression);
+ const options: IrExpr = args[1]
+ ? lowerer.lowerExprExpecting(args[1], DYN)
+ : {
+ kind: "dynFrom",
+ value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
+ type: DYN,
+ loc,
+ };
+ return {
+ kind: "libCall",
+ fn: "worker.new",
+ args: [
+ { kind: "numLit", value: root, type: F64, loc },
+ { kind: "strLit", value: path!, type: STRING, loc },
+ options,
+ ],
+ type: DYN,
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts
index adc0afc4f..5b8285f12 100644
--- a/packages/compiler/src/frontend/lowering/containers/bytes.ts
+++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts
@@ -2,7 +2,6 @@ import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js";
import * as ts from "../../ts7/adapter.js";
import {
- BYTES_ELEMENT_SIZE,
BIGINT_T,
BOOL,
BYTES_U8,
@@ -153,7 +152,11 @@ export function lowerBytesNew(
expr: ts.NewExpression,
symbol: ts.Symbol | null | undefined,
): IrExpr | null {
- if (symbol != null && symbol.name === "ArrayBuffer" && lowerer.isStdlibSymbol(symbol)) {
+ if (
+ symbol != null &&
+ (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer") &&
+ lowerer.isStdlibSymbol(symbol)
+ ) {
const args = expr.arguments ?? [];
if (args.length > 1 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(
@@ -165,7 +168,13 @@ export function lowerBytesNew(
const length = args[0]
? lowerer.coerceInto(args[0], lowerer.lowerExpr(args[0]), DYN)
: dynUndefinedExpr(locOf(expr));
- return { kind: "libCall", fn: "arrayBuffer.new", args: [length], type: DYN, loc: locOf(expr) };
+ return {
+ kind: "libCall",
+ fn: symbol.name === "SharedArrayBuffer" ? "sharedArrayBuffer.new" : "arrayBuffer.new",
+ args: [length],
+ type: DYN,
+ loc: locOf(expr),
+ };
}
if (symbol && symbol.name === "DataView" && lowerer.isStdlibSymbol(symbol)) {
return lowerDataViewNew(lowerer, expr);
@@ -187,46 +196,6 @@ export function lowerBytesNew(
const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
return { kind: "bytesNew", source: seed, type, loc };
}
- // The SYNTACTIC `new T(new SharedArrayBuffer(n))` form — fresh-buffer construction (the
- // shared spelling is the Atomics.wait sleep idiom's). The buffer
- // never exists as a value: n must be a byte-length LITERAL divisible
- // by the element size (tsc would admit any number; a bad one is
- // Node's RangeError — rejected at compile time instead of
- // half-lowering), and the whole expression is a zero-filled typed
- // array of n/elemSize elements. Erasing the buffer is exact: nothing
- // else can ever reference it, so neither sharing (scriptc has no
- // threads) nor aliasing is observable — SEMANTICS.md documents the
- // stance. The RESIZABLE form (a maxByteLength options bag) fences by
- // name: this erasure has no shared or growable buffer representation.
- if (
- ts.isNewExpression(argNode) &&
- ts.isIdentifier(argNode.expression) &&
- argNode.expression.text === "SharedArrayBuffer" &&
- lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode.expression) ?? undefined)
- ) {
- const bufCtor = argNode.expression.text;
- if ((argNode.arguments?.length ?? 0) > 1) {
- lowerer.noLowering(
- `new ${name} over a resizable ${bufCtor}`,
- argNode,
- "a maxByteLength options bag needs growable shared storage; the fixed-length " +
- "SharedArrayBuffer here erases into the view: drop the options bag",
- );
- }
- const elemSize = BYTES_ELEMENT_SIZE[elem];
- const lenArg = argNode.arguments?.length === 1 ? argNode.arguments[0] : undefined;
- const lenT = lenArg ? lowerer.typeOf(lenArg) : null;
- const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null;
- if (byteLen === null || byteLen % elemSize !== 0 || byteLen < 0) {
- lowerer.noLowering(
- `new ${name} over this ${bufCtor}`,
- argNode,
- `the byte length must be a number literal divisible by ${elemSize} — new ${name}(new ${bufCtor}(${elemSize}))`,
- );
- }
- const count: IrExpr = { kind: "numLit", value: byteLen / elemSize, type: F64, loc };
- return { kind: "bytesNew", source: count, type, loc };
- }
const src = lowerer.lowerExpr(argNode);
if (src.type.kind === "union") {
const arms = lowerer.unions.get(src.type.unionId)?.arms;
@@ -1303,7 +1272,8 @@ function lowerBufferStaticValue(
args.length >= 1 &&
args.length <= 3 &&
!args.some(ts.isSpreadElement) &&
- ((symbol) => symbol != null && symbol.name === "ArrayBuffer")(
+ ((symbol) =>
+ symbol != null && (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer"))(
lowerer.typeOf(args[0]!).getSymbol(),
)
) {
diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
index a7ba44d10..3829c846c 100644
--- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
+++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
@@ -1,9 +1,9 @@
+import { lowerWorkerMetadata } from "./builtins/workers.js";
import * as ts from "../ts7/adapter.js";
import {
BOOL,
DYN,
F64,
- NULL_T,
STRING,
UNDEFINED_T,
arrayOf,
@@ -416,8 +416,9 @@ export function lowerNumberParserValue(
};
}
-// These module values expose existing native callable lowerings and main-thread
-// metadata. Other exports keep an explicit runtime refusal, including Worker.
+// These module values expose native callable lowerings and current-thread
+// metadata. Stored Worker constructors remain refused: native worker roots
+// must be resolved from the source at compilation time.
const MODULES = ["path/posix", "path/win32", "os", "worker_threads", "util/types"] as const;
const box = (value: IrExpr): IrExpr =>
@@ -515,17 +516,18 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr {
if (callable) add(member, callable);
}
for (const member of Object.keys(BUILTIN_MODULE_CONSTS[module] ?? {})) {
+ const worker = module === "worker_threads" ? lowerWorkerMetadata(member, loc) : null;
const value = builtinModuleConstOf(lowerer, module, member);
- if (value !== undefined) add(member, builtinConstLit(value, loc));
+ if (worker) add(member, worker);
+ else if (value !== undefined) add(member, builtinConstLit(value, loc));
}
if (module.startsWith("path/")) {
add("posix", moduleValue(lowerer, "path/posix", loc));
add("win32", moduleValue(lowerer, "path/win32", loc));
}
if (module === "worker_threads") {
- add("isInternalThread", { kind: "boolLit", value: false, type: BOOL, loc });
- add("parentPort", { kind: "unitLit", unit: "null", type: NULL_T, loc });
- add("workerData", { kind: "unitLit", unit: "null", type: NULL_T, loc });
+ for (const member of ["isInternalThread", "parentPort", "workerData"])
+ add(member, lowerWorkerMetadata(member, loc)!);
}
body.push({
kind: "return",
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index bb81dae39..225e13aec 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -3411,7 +3411,15 @@ function lowerDynReceiverMethodCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (lowerer.chainBlocked(call, access)) return null;
- if (stdlibGlobalNameOf(lowerer, access.expression) !== null) return null;
+ if (stdlibGlobalNameOf(lowerer, access.expression) !== null) {
+ // A named builtin import can resolve to its ambient declaration, too.
+ // parentPort is an instance value whose methods use the native handle;
+ // it must work without a non-null assertion or a namespace spelling.
+ const binding = ts.isIdentifier(access.expression)
+ ? lowerer.builtinImportOf(access.expression)
+ : null;
+ if (binding?.module !== "worker_threads" || binding.member !== "parentPort") return null;
+ }
// Only checker-untyped receivers: `any`/`unknown`, or the `any[]` an
// Array.isArray guard narrows them to (the value is STILL the checked-dynamic tree
// array — scalar narrowings bridge through maybeNarrow's dynCheck and
@@ -4034,6 +4042,11 @@ const DYN_DISPATCH_METHODS = new Set([
"additionalHeaders",
"altsvc",
"origin",
+ // Worker and MessagePort methods use the owning context's handle adapter.
+ "postMessage",
+ "terminate",
+ "start",
+ "hasRef",
]);
export function lowerDynDispatchMethodCall(
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index f06a5e544..452099ff1 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -73,6 +73,7 @@ import { bufEncoding } from "./containers/bytes.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { lowerSearchParamsNew } from "./builtins/url.js";
import { lowerTextCodecNew } from "./builtins/text-codecs.js";
+import { lowerWorkerNew } from "./builtins/workers.js";
import {
requiresDynamicPackageDiag,
unsupportedDiag,
@@ -7905,6 +7906,8 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
+ const worker = lowerWorkerNew(lowerer, expr);
+ if (worker) return worker;
let intlConstructor = expr.expression;
if (ts.isIdentifier(intlConstructor)) {
const symbol = lowerer.resolveValueSymbol(intlConstructor);
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 6e7f17598..6afd99152 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -1,3 +1,4 @@
+import { lowerWorkerMetadata } from "./builtins/workers.js";
import {
dynUndefinedExpr,
nodeThrowExpr,
@@ -1398,6 +1399,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
{
const bi = lowerer.builtinImportOf(expr);
if (bi) {
+ if (bi.module === "worker_threads") {
+ const value = lowerWorkerMetadata(bi.member, loc);
+ if (value) return value;
+ }
const c = builtinModuleConstOf(lowerer, bi.module, bi.member);
if (c !== undefined) return builtinConstLit(c, loc);
// module.builtinModules — the baked Node v24 list, a fresh
@@ -1632,8 +1637,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
Reflect:
"reflective property access has no static lowering — read and call members directly",
Intl: 'native Intl supports default Unicode grapheme segmentation and the composed new Intl.NumberFormat("en-US").format(x); locale negotiation and other ICU-backed operations remain unsupported',
- SharedArrayBuffer:
- "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage",
+ SharedArrayBuffer: "use direct new SharedArrayBuffer(length) for fixed shared storage",
ArrayBuffer:
"ArrayBuffer constructor values have no native lowering; use new ArrayBuffer(length) for fixed-length shared storage",
WeakRef:
@@ -11603,6 +11607,10 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return { kind: "libCall", fn: "dyn.dataViewIs", args: [value], type: BOOL, loc };
}
+ if (lowerer.isStdlibGlobal(expr.right, "SharedArrayBuffer")) {
+ const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
+ return { kind: "libCall", fn: "sharedArrayBuffer.is", args: [value], type: BOOL, loc };
+ }
if (lowerer.isStdlibGlobal(expr.right, "ArrayBuffer")) {
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return { kind: "libCall", fn: "arrayBuffer.is", args: [value], type: BOOL, loc };
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index 3ca4be062..3efbbdae7 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -127,6 +127,7 @@ import {
import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js";
import { cjsModuleRef, cjsModuleRegistryPrelude } from "./lower-node-module.js";
import { forkTargetPaths } from "../fork-target.js";
+import { workerTargetPaths } from "../worker-target.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
/** One file's declarations, split for collection and init-body lowering. */
@@ -147,6 +148,25 @@ export function appendForkModules(
order: ts.SourceFile[],
targets: ts.SourceFile[],
onCycle: (cycle: string, reason: string) => void,
+): void {
+ appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle);
+}
+
+export function appendWorkerModules(
+ program: ts.Program,
+ order: ts.SourceFile[],
+ targets: ts.SourceFile[],
+ onCycle: (cycle: string, reason: string) => void,
+): void {
+ appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle);
+}
+
+function appendEntryModules(
+ program: ts.Program,
+ order: ts.SourceFile[],
+ targets: ts.SourceFile[],
+ paths: readonly string[],
+ onCycle: (cycle: string, reason: string) => void,
): void {
if (order.length === 0) return;
const byPath = new Map(
@@ -187,7 +207,7 @@ export function appendForkModules(
};
const knownTargets = new Set(targets);
- for (const path of forkTargetPaths(program, order)) {
+ for (const path of paths) {
const target = byPath.get(resolvePath(path));
if (!target || target.isDeclarationFile || target.fileName.endsWith(".json")) continue;
if (!knownTargets.has(target)) {
@@ -3145,7 +3165,7 @@ export function lowerFileInit(
export function buildMain(lowerer: Lowerer): IrFunction {
const loc: SrcLoc = { file: lowerer.entry.fileName, start: 0, end: 0 };
const body: IrStmt[] = [...cjsModuleRegistryPrelude(lowerer, loc)];
- const roots = [lowerer.entry, ...lowerer.forkTargets];
+ const roots = [lowerer.entry, ...lowerer.forkTargets, ...lowerer.workerTargets];
const isAsync = roots.some((root) => lowerer.asyncInitFiles.has(root));
const callRoot = (root: ts.SourceFile): IrStmt[] => {
const init = lowerer.initNameOf.get(root);
@@ -3219,6 +3239,35 @@ export function buildMain(lowerer: Lowerer): IrFunction {
loc,
});
}
+ if (lowerer.workerTargets.length > 0) {
+ const mainBody = body.splice(0);
+ let workerBody: IrStmt[] | null = null;
+ for (let id = lowerer.workerTargets.length - 1; id >= 0; id--) {
+ workerBody = [
+ {
+ kind: "if",
+ cond: {
+ kind: "bin",
+ op: "===",
+ left: { kind: "libCall", fn: "worker.root", args: [], type: F64, loc },
+ right: { kind: "numLit", value: id, type: F64, loc },
+ type: BOOL,
+ loc,
+ },
+ then: callRoot(lowerer.workerTargets[id]!),
+ else_: workerBody,
+ loc,
+ },
+ ];
+ }
+ body.push({
+ kind: "if",
+ cond: { kind: "libCall", fn: "worker.isMainThread", args: [], type: BOOL, loc },
+ then: mainBody,
+ else_: [...cjsModuleRegistryPrelude(lowerer, loc), ...(workerBody ?? [])],
+ loc,
+ });
+ }
// Node's startup refusal (a resolution the graph carries that Node
// rejects — preflight's Node-order resolution walk — or a module-LINK
// SyntaxError from either named-import checker): the graph is refused
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index d37af07cc..c71007209 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -1,3 +1,4 @@
+import { lowerWorkerMetadata } from "./builtins/workers.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
import type { FieldLift } from "./coercions/structural-plans.js";
import { lowerBuiltinCall } from "./builtin-calls.js";
@@ -233,6 +234,7 @@ import {
buildMain,
appendDynamicImportModules,
appendForkModules,
+ appendWorkerModules,
} from "./lower-modules.js";
import { prepareCjsModuleGraph } from "./lower-node-module.js";
import {
@@ -805,6 +807,7 @@ export interface LowererMode {
libraryCallbacks?: boolean;
/** Statically resolved child_process.fork program roots, in stable target-id order. */
forkTargets?: readonly ts.SourceFile[];
+ workerTargets?: readonly ts.SourceFile[];
/** Program-validated ambient declaration symbols for each FFI name.
* Undefined in discovery's legacy call-local validation path. */
ffiBindingSymbols?: ReadonlyMap>;
@@ -878,6 +881,7 @@ export function lowerToIr(
const extraModuleCycleDiags: ScrDiagnostic[] = [];
const cycleKeys = new Set();
const forkTargets: ts.SourceFile[] = [];
+ const workerTargets: ts.SourceFile[] = [];
const onExtraCycle = (cycle: string, reason: string): void => {
const key = `${cycle}\0${reason}`;
if (cycleKeys.has(key)) return;
@@ -894,6 +898,7 @@ export function lowerToIr(
const before = moduleOrder.length;
appendDynamicImportModules(program, moduleOrder, onExtraCycle);
appendForkModules(program, moduleOrder, forkTargets, onExtraCycle);
+ appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle);
if (moduleOrder.length === before) break;
if (pass === 31) throw new Error("dynamic import and fork module discovery did not converge");
}
@@ -909,6 +914,7 @@ export function lowerToIr(
ffiImports,
libraryCallbacks,
forkTargets,
+ workerTargets,
externalTypes,
externalTypeSpecifiersByFile,
});
@@ -929,6 +935,7 @@ export function lowerToIr(
ffiImports,
libraryCallbacks,
forkTargets,
+ workerTargets,
externalTypes,
externalTypeSpecifiersByFile,
ffiBindingSymbols: ffiValidation.symbolsByName,
@@ -956,6 +963,7 @@ export function lowerToIr(
ffiImports,
libraryCallbacks,
forkTargets,
+ workerTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
@@ -976,6 +984,7 @@ export function lowerToIr(
ffiImports,
libraryCallbacks,
forkTargets,
+ workerTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
@@ -2324,6 +2333,9 @@ export class Lowerer {
readonly fileTag = new Map();
/** Embedded fork roots and their private startup ids. */
readonly forkTargets: readonly ts.SourceFile[];
+ readonly workerTargets: readonly ts.SourceFile[];
+ readonly workerTargetIdByPath = new Map();
+ usesWorkers = false;
readonly forkTargetIdByPath = new Map();
/** Namespace ModuleBlocks this program lowers, filled by splitFiles:
* "flattened" — an instantiated namespace whose body joined the file's
@@ -2552,6 +2564,10 @@ export class Lowerer {
this.startupCrash = mode.startupCrash ?? null;
this.ffiImports = mode.ffiImports ?? [];
this.libraryCallbacks = mode.libraryCallbacks ?? false;
+ this.workerTargets = mode.workerTargets ?? [];
+ this.workerTargets.forEach((sf, id) => {
+ this.workerTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id);
+ });
this.forkTargets = mode.forkTargets ?? [];
this.forkTargets.forEach((sf, id) => {
this.forkTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id);
@@ -4871,7 +4887,7 @@ export class Lowerer {
this.diags.length > 0
? null
: {
- irVersion: 13,
+ irVersion: 14,
sourceFile: this.entry.fileName,
functions,
classes: artifacts.classes,
@@ -4880,6 +4896,7 @@ export class Lowerer {
globals: this.globalsList,
...(this.npmEmbedded ? { embedded: this.npmEmbedded } : {}),
entry: ENTRY_NAME,
+ ...(this.usesWorkers ? { workers: true } : {}),
...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}),
};
if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name));
@@ -5806,10 +5823,9 @@ export class Lowerer {
"the rest is ICU locale data the binary does not carry";
const typeHints: Record = {
ArrayBuffer:
- "no free-standing ArrayBuffer value exists — typed arrays own their storage " +
- "(new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view)",
+ "fixed ArrayBuffer storage and typed-array views are supported; resizable and detached storage are not modeled",
SharedArrayBuffer:
- "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage",
+ "fixed SharedArrayBuffer storage is supported; growable shared storage is not modeled",
NumberFormat: intlHint,
DateTimeFormat: intlHint,
DurationFormat: intlHint,
@@ -10161,6 +10177,10 @@ export class Lowerer {
if (bi.module === "events" && bi.member === "defaultMaxListeners") {
return { kind: "libCall", fn: "emitter.getDefaultMax", args: [], type: F64, loc };
}
+ if (bi.module === "worker_threads") {
+ const value = lowerWorkerMetadata(bi.member, loc);
+ if (value) return value;
+ }
const c = builtinModuleConstOf(this, bi.module, bi.member);
if (c !== undefined) return builtinConstLit(c, loc);
// module.builtinModules through a namespace binding — the baked
diff --git a/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts b/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts
index 7b902b489..31d51d7d7 100644
--- a/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts
+++ b/packages/compiler/src/frontend/lowering/sanitize-class-types.test.ts
@@ -21,7 +21,7 @@ test("fenced instance slots are erased throughout signatures, storage and nested
};
const ref = (type: IrType): IrExpr => ({ kind: "varRef", localId: "x", type, loc });
const module: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
globals: [{ id: "g", name: "g", type: composite, mutable: false }],
@@ -110,7 +110,7 @@ test("modules without optional tables and registered class types retain their st
const loc = { file: "registered.ts", start: 0, end: 1 };
const type: IrType = { kind: "object", className: "Registered" };
const module: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index 39d21b792..55453eddb 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -1640,10 +1640,9 @@ export const BUILTIN_MODULE_FNS: Record<
* a literal of its value's kind. The bare-module values are the TARGET
* platform's (builtinModuleConstOf swaps the win32 values in under a win32
* triple — Node's own platform conditionals); the named namespaces answer
- * THEIR platform's constants everywhere. worker_threads/cluster carry the
- * main-thread truths: a compiled binary IS the main thread (no JS-engine
- * thread machinery) and never runs as a cluster worker (cluster forks the
- * node binary itself) — Node's own answers for a directly-run script. */
+ * THEIR platform's constants everywhere. Worker metadata uses these names
+ * for inventory but reads the current runtime context in its dedicated
+ * lowering. Cluster metadata describes a directly-run native executable. */
export const BUILTIN_MODULE_CONSTS: Record<
string,
Record | undefined
@@ -1754,6 +1753,34 @@ export interface AmbientSurfaceRow {
}
export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
+ {
+ id: "node-builtin.worker_threads.Worker",
+ kind: "node-builtin",
+ name: "worker_threads.Worker",
+ fns: ["worker.new"],
+ note: "native threads with statically compiled entry points, workerData, argv and message events",
+ },
+ {
+ id: "node-builtin.worker_threads.metadata",
+ kind: "node-builtin",
+ name: "worker_threads metadata",
+ fns: ["worker.isMainThread", "worker.threadId", "worker.data", "worker.parentPort"],
+ note: "current-thread metadata and the worker's parent message port",
+ },
+ {
+ id: "stdlib.sharedArrayBuffer",
+ kind: "stdlib",
+ name: "SharedArrayBuffer",
+ fns: ["sharedArrayBuffer.new"],
+ note: "fixed shared storage with context-local typed-array and DataView wrappers",
+ },
+ {
+ id: "stdlib.atomics",
+ kind: "stdlib",
+ name: "Atomics",
+ fns: ["atomics.op", "atomics.wait", "atomics.notify"],
+ note: "8-, 16- and 32-bit integer operations, plus Int32Array wait and notify",
+ },
{
id: "node-builtin.fs.callbacks",
kind: "node-builtin",
@@ -1945,7 +1972,7 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
kind: "node-builtin",
name: "process.getBuiltinModule",
fns: ["process.builtinId", "process.builtinModule", "process.builtinUnsupported"],
- note: "native path and os export subsets plus main-thread worker_threads metadata; other modules and exports throw SC2020",
+ note: "native path and os export subsets plus current-thread worker_threads metadata; other modules and exports throw SC2020",
},
{
id: "node-builtin.process.versions",
@@ -2608,8 +2635,7 @@ export function stdlibMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExp
"allocate a new buffer and copy through typed-array views instead";
} else if (container === "SharedArrayBuffer" || container.startsWith("SharedArrayBuffer<")) {
hint =
- "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage " +
- "(a fixed-length allocation: grow has nothing to share it with)";
+ "native SharedArrayBuffer storage is fixed-length; growable shared buffers are unsupported";
} else if (
container === "Intl" ||
container.startsWith("Intl.") ||
diff --git a/packages/compiler/src/frontend/module-source-candidates.ts b/packages/compiler/src/frontend/module-source-candidates.ts
index 4310bc843..dd468c421 100644
--- a/packages/compiler/src/frontend/module-source-candidates.ts
+++ b/packages/compiler/src/frontend/module-source-candidates.ts
@@ -2,6 +2,7 @@ import * as ts from "./ts7/adapter.js";
export interface ModuleSourceCandidates {
calls: ts.CallExpression[];
+ constructions: ts.NewExpression[];
specifiers: string[];
}
@@ -9,6 +10,7 @@ const MODULE_SOURCE_KINDS: ReadonlySet = new Set([
ts.SyntaxKind.ImportDeclaration,
ts.SyntaxKind.ExportDeclaration,
ts.SyntaxKind.CallExpression,
+ ts.SyntaxKind.NewExpression,
]);
/** Root discovery and import admission inspect the same immutable syntax.
@@ -20,7 +22,7 @@ export function moduleSourceCandidates(
): ModuleSourceCandidates {
const cached = program.analysis.moduleSourceCandidates.get(source);
if (cached !== undefined) return cached;
- const result: ModuleSourceCandidates = { calls: [], specifiers: [] };
+ const result: ModuleSourceCandidates = { calls: [], constructions: [], specifiers: [] };
ts.walkPreorder(
source,
(node) => {
@@ -44,6 +46,7 @@ export function moduleSourceCandidates(
result.specifiers.push(arg.text);
}
}
+ if (ts.isNewExpression(node)) result.constructions.push(node);
return undefined;
},
MODULE_SOURCE_KINDS,
diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts
index 9744e38b3..f2ce119a9 100644
--- a/packages/compiler/src/frontend/program.ts
+++ b/packages/compiler/src/frontend/program.ts
@@ -117,6 +117,7 @@ import {
} from "./tsc-codes.js";
import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
import { forkTargetPaths } from "./fork-target.js";
+import { workerTargetPaths } from "./worker-target.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
@@ -669,6 +670,7 @@ function loadProgram7(
...entryPackageProgramRoots7(program, entryPath),
...createRequireProgramRoots7(program),
...forkTargetPaths(program, program.getImplementationSourceFiles()),
+ ...workerTargetPaths(program, program.getImplementationSourceFiles()),
];
const extraRoots = candidates.filter(
(root, index) => !programRoots.includes(root) && candidates.indexOf(root) === index,
@@ -2693,6 +2695,7 @@ function preflight7(load: LoadResult): {
[
...createRequireProgramRoots7(program),
...forkTargetPaths(program, program.getImplementationSourceFiles()),
+ ...workerTargetPaths(program, program.getImplementationSourceFiles()),
],
(sf, spec, resolutionKind) =>
resolveImport7(program, sf, spec, resolutionKind) ??
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index 2bcebe1ef..7168390b0 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -2161,6 +2161,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
if (isStdlibInterface("Immediate")) return F64;
if (
isStdlibInterface("ArrayBuffer") ||
+ isStdlibInterface("SharedArrayBuffer") ||
isStdlibInterface("PropertyDescriptor") ||
isStdlibInterface("ProcessVersions")
)
@@ -2263,6 +2264,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
)
)
return DYN;
+ if (
+ (psym?.name === "Worker" || psym?.name === "MessagePort") &&
+ checker
+ .declarationsOf(psym)
+ .some(
+ (d) =>
+ (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
+ ctx.isStdlibFile(d.getSourceFile()) &&
+ isDeclaredInAmbientModule(d, "worker_threads"),
+ )
+ )
+ return DYN;
// fs/promises.FileHandle: an owned descriptor object. Module provenance
// distinguishes it from user interfaces with the same name; both the
// fallback and @types/node declare it in "fs/promises".
diff --git a/packages/compiler/src/frontend/worker-target.test.ts b/packages/compiler/src/frontend/worker-target.test.ts
new file mode 100644
index 000000000..7125bc309
--- /dev/null
+++ b/packages/compiler/src/frontend/worker-target.test.ts
@@ -0,0 +1,81 @@
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { expect, test } from "vitest";
+import { workerTargetPaths } from "./worker-target.js";
+import { fallbackDtsPath } from "./dts-paths.js";
+import { Ts7Host } from "./ts7/program-host.js";
+import { Ts7Api } from "./ts7/rpc-api.js";
+import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js";
+
+function discover(source: string): string[] {
+ const directory = mkdtempSync(join(tmpdir(), "scriptc-worker-discovery-"));
+ const entry = join(directory, "main.ts");
+ writeFileSync(entry, source);
+ writeFileSync(
+ join(directory, "facade.ts"),
+ 'export { Worker as Task } from "node:worker_threads";',
+ );
+ const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory });
+ const program = host.createProgram([entry, fallbackDtsPath()], {
+ noLib: true,
+ types: [],
+ module: ModuleKind.ESNext,
+ moduleResolution: ModuleResolutionKind.Bundler,
+ });
+ try {
+ return workerTargetPaths(program, [program.getSourceFile(entry)!]).map((file) =>
+ file.replace(directory + "/", ""),
+ );
+ } finally {
+ program.dispose();
+ host.close();
+ rmSync(directory, { recursive: true, force: true });
+ }
+}
+
+test("worker discovery preserves imports, facades, CommonJS bindings and distinct roots", () => {
+ expect(
+ discover(`
+ import { Worker as Start } from "node:worker_threads";
+ import * as threads from "worker_threads";
+ import workers from "node:worker_threads";
+ import { Task } from "./facade.js";
+ const { Worker: Required } = require("node:worker_threads");
+ const module = require("worker_threads");
+ const Constructor = module.Worker;
+ new Start(new URL("./one.ts", import.meta.url));
+ new threads.Worker(new URL("./two.ts", import.meta.url));
+ new workers.Worker(new URL("./three.ts", import.meta.url));
+ new Task(new URL("./four.ts", import.meta.url));
+ new Required(__filename);
+ new Constructor(new URL(import.meta.url));
+ new (require("worker_threads").Worker)("/absolute/worker.ts");
+ new Start(new URL("./one.ts", import.meta.url));
+ `),
+ ).toEqual(["one.ts", "two.ts", "three.ts", "four.ts", "main.ts", "/absolute/worker.ts"]);
+});
+
+test("worker discovery rejects dynamic roots and shadowed globals or constructors", () => {
+ expect(
+ discover(`
+ import { Worker } from "node:worker_threads";
+ new Worker("./cwd-dependent.ts");
+ let filename = "/mutable.ts";
+ new Worker(filename);
+ function local(Worker: any) { new Worker("/local.ts"); }
+ function loader(require: any) {
+ const { Worker: Local } = require("worker_threads");
+ new Local("/shadowed.ts");
+ }
+ function path(__filename: string) { new Worker(__filename); }
+ function shadowURL(URL: any) { new Worker(new URL("/wrong.ts")); }
+ let { Worker: Mutable } = require("worker_threads");
+ new Mutable("/mutable-constructor.ts");
+ let namespace = require("worker_threads");
+ new namespace.Worker("/mutable-namespace.ts");
+ const unrelated = { Worker: class {} };
+ new unrelated.Worker("/unrelated.ts");
+ `),
+ ).toEqual([]);
+});
diff --git a/packages/compiler/src/frontend/worker-target.ts b/packages/compiler/src/frontend/worker-target.ts
new file mode 100644
index 000000000..a139da6e8
--- /dev/null
+++ b/packages/compiler/src/frontend/worker-target.ts
@@ -0,0 +1,123 @@
+import { isAbsolute, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import * as ts from "./ts7/adapter.js";
+import {
+ isNodeGlobal,
+ isBuiltinMemberImport,
+ isBuiltinNamespaceImport,
+ staticForkString,
+} from "./fork-target.js";
+import { moduleSourceCandidates } from "./module-source-candidates.js";
+import { canonicalBuiltinModule } from "./builtin-modules.js";
+
+function workerNamespace(program: ts.Program, expression: ts.Expression): boolean {
+ if (
+ ts.isCallExpression(expression) &&
+ ts.isIdentifier(expression.expression) &&
+ expression.expression.text === "require" &&
+ isNodeGlobal(program, expression.expression) &&
+ expression.arguments.length === 1 &&
+ ts.isStringLiteralLike(expression.arguments[0]!)
+ )
+ return canonicalBuiltinModule(expression.arguments[0]!.text) === "worker_threads";
+ if (!ts.isIdentifier(expression)) return false;
+ if (isBuiltinNamespaceImport(program, expression, "worker_threads")) return true;
+ const checker = program.getTypeChecker(),
+ symbol = checker.getSymbolAtLocation(expression);
+ const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined;
+ return (
+ declaration !== undefined &&
+ ts.isVariableDeclaration(declaration) &&
+ ts.isVariableDeclarationList(declaration.parent) &&
+ (declaration.parent.flags & ts.NodeFlags.Const) !== 0 &&
+ declaration.initializer !== undefined &&
+ ts.isCallExpression(declaration.initializer) &&
+ workerNamespace(program, declaration.initializer)
+ );
+}
+
+/** Share constructor provenance between root discovery and lowering. Local
+ * classes and shadowed require functions must never add native worker roots. */
+export function isWorkerConstructor(program: ts.Program, expression: ts.Expression): boolean {
+ while (
+ ts.isParenthesizedExpression(expression) ||
+ ts.isAsExpression(expression) ||
+ ts.isTypeAssertion(expression) ||
+ ts.isNonNullExpression(expression)
+ )
+ expression = expression.expression;
+ if (ts.isPropertyAccessExpression(expression))
+ return expression.name.text === "Worker" && workerNamespace(program, expression.expression);
+ if (!ts.isIdentifier(expression)) return false;
+ if (isBuiltinMemberImport(program, expression, "worker_threads", "Worker")) return true;
+ const checker = program.getTypeChecker(),
+ symbol = checker.getSymbolAtLocation(expression);
+ const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined;
+ if (
+ declaration &&
+ ts.isBindingElement(declaration) &&
+ ts.isObjectBindingPattern(declaration.parent)
+ ) {
+ const variable = declaration.parent.parent;
+ const member = declaration.propertyName ?? declaration.name;
+ return (
+ ts.isIdentifier(member) &&
+ member.text === "Worker" &&
+ ts.isVariableDeclaration(variable) &&
+ ts.isVariableDeclarationList(variable.parent) &&
+ (variable.parent.flags & ts.NodeFlags.Const) !== 0 &&
+ variable.initializer !== undefined &&
+ workerNamespace(program, variable.initializer)
+ );
+ }
+ return (
+ declaration !== undefined &&
+ ts.isVariableDeclaration(declaration) &&
+ ts.isVariableDeclarationList(declaration.parent) &&
+ (declaration.parent.flags & ts.NodeFlags.Const) !== 0 &&
+ declaration.initializer !== undefined &&
+ ts.isPropertyAccessExpression(declaration.initializer) &&
+ isWorkerConstructor(program, declaration.initializer)
+ );
+}
+
+export function workerModulePath(program: ts.Program, expression: ts.Expression): string | null {
+ if (ts.isIdentifier(expression) && expression.text === "__filename") {
+ if (isNodeGlobal(program, expression)) return resolve(expression.getSourceFile().fileName);
+ }
+ const value = staticForkString(program, expression);
+ if (value === null) return null;
+ try {
+ if (value.startsWith("file:")) return fileURLToPath(value);
+ // Relative string filenames are resolved against the invocation's cwd in
+ // Node. Only absolute paths and source-relative URLs fix the build graph.
+ return isAbsolute(value) ? resolve(value) : null;
+ } catch {
+ return null;
+ }
+}
+
+export function workerTargetPaths(program: ts.Program, files: readonly ts.SourceFile[]): string[] {
+ const targets = new Set();
+ const checker = program.getTypeChecker();
+ for (const source of files) {
+ if (source.isDeclarationFile || source.fileName.endsWith(".json")) continue;
+ const constructions = moduleSourceCandidates(program, source).constructions;
+ checker.prefetchSymbolNodesExact(
+ constructions.flatMap(({ expression }) =>
+ ts.isIdentifier(expression)
+ ? [expression]
+ : ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)
+ ? [expression.expression]
+ : [],
+ ),
+ );
+ for (const construction of constructions) {
+ const expression = construction.expression;
+ if (!isWorkerConstructor(program, expression) || !construction.arguments?.[0]) continue;
+ const path = workerModulePath(program, construction.arguments[0]);
+ if (path !== null) targets.add(path);
+ }
+ }
+ return [...targets];
+}
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index 78fb4df08..2e48a17fd 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -451,6 +451,7 @@ async function compileExecutableNative(
dgram: features.dgram,
watch: features.watch,
foreignFfi: features.foreignFfi,
+ ...(features.workers ? { workers: true } : {}),
nodeTest: features.nodeTest,
tls: features.tls,
tlsCa: features.tlsCa,
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index a3fa16612..076d37221 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -8,6 +8,10 @@ import type { IrLibFn } from "./builtin-signatures.js";
* seed on `dynCheck` and `awaitExpr` nodes, which throw on validation
* failure / promise rejection). */
export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
+ "worker.new",
+ "worker.data",
+ "worker.parentPort",
+ "process.exit",
"text.encodeInto",
"buffer.isAscii",
"buffer.isUtf8",
@@ -40,6 +44,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"buffer.byteLenDyn",
"bytes.construct",
"arrayBuffer.new",
+ "sharedArrayBuffer.new",
+ "atomics.op",
+ "atomics.wait",
+ "atomics.notify",
"arrayBuffer.byteLengthGetter",
"arrayBuffer.viewU8C",
"arrayBuffer.viewI8",
@@ -442,6 +450,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"dyn.getOwnPropertyDescriptor",
"dyn.arrayProtoCall",
"process.chdir",
+ "process.umask",
"process.loadEnvFile",
"fs.realpathSync",
"fs.realpathNativeSync",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 7f73ba9db..03d4a3a91 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -923,19 +923,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"fs.writeFileModeSync": { argTypes: [STRING, STRING, F64], result: VOID },
"fs.mkdirModeSync": { argTypes: [STRING, F64], result: VOID },
"fs.mkdirRecursiveModeSync": { argTypes: [STRING, F64], result: VOID },
- // Atomics.wait over an Int32Array — the synchronous-sleep idiom.
- /** Atomics.wait(int32Array, idx, expected, timeoutMs) → "not-equal"
- * when the element differs from `expected`, else a real nanosleep for
- * the timeout and "timed-out" (scr_lib.c). scriptc has no threads —
- * no other agent can ever notify, so for every compilable program this
- * IS the spec's behavior, and "ok" is unreachable; the frontend
- * REQUIRES the timeout argument (an infinite wait here is a certain
- * deadlock, fenced). The SharedArrayBuffer the lib types demand exists
- * only syntactically (new Int32Array(new SharedArrayBuffer(n)) lowers
- * to a plain i32 typed array — sharing is unobservable without
- * threads; SEMANTICS.md documents the stance). Never throws; +1 string
- * result. */
"atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING },
+ "atomics.notify": { argTypes: [bytesOf("i32"), F64, F64], result: F64 },
+ "atomics.op": { argTypes: [null, F64, F64, F64, F64], result: F64 },
+ "worker.new": { argTypes: [F64, STRING, DYN], result: DYN },
+ "worker.isMainThread": { argTypes: [], result: BOOL },
+ "worker.threadId": { argTypes: [], result: F64 },
+ "worker.root": { argTypes: [], result: F64 },
+ "worker.data": { argTypes: [], result: DYN },
+ "worker.parentPort": { argTypes: [], result: DYN },
/** child_process.spawnSync (scr_child.c): posix_spawn + waitpid + piped
* utf8 capture — cmd borrowed, args one borrowed string[] (the frontend
* completes an omitted list to an empty literal), result an owned (+1)
@@ -2611,6 +2607,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.dataViewIs": { argTypes: [DYN], result: BOOL },
"dyn.construct": { argTypes: [DYN, DYN, STRING], result: DYN },
"arrayBuffer.constructor": { argTypes: [], result: DYN },
+ "sharedArrayBuffer.new": { argTypes: [DYN], result: DYN },
+ "sharedArrayBuffer.is": { argTypes: [DYN], result: BOOL },
"arrayBuffer.new": { argTypes: [DYN], result: DYN },
"ffi.argument": { argTypes: [DYN, STRING], result: DYN },
"ffi.memoryModule": { argTypes: [DYN], result: DYN },
diff --git a/packages/compiler/src/ir/constant-tables.test.ts b/packages/compiler/src/ir/constant-tables.test.ts
index 0ce09d881..c1bc9a094 100644
--- a/packages/compiler/src/ir/constant-tables.test.ts
+++ b/packages/compiler/src/ir/constant-tables.test.ts
@@ -20,7 +20,7 @@ const expr = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: value, loc })
function fixture(): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
globals: [{ id, name: "table", type, mutable: false }],
diff --git a/packages/compiler/src/ir/initializer-bindings.test.ts b/packages/compiler/src/ir/initializer-bindings.test.ts
index 8a070e819..6891935c5 100644
--- a/packages/compiler/src/ir/initializer-bindings.test.ts
+++ b/packages/compiler/src/ir/initializer-bindings.test.ts
@@ -36,7 +36,7 @@ function fixture(): IrModule {
],
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: fn.name,
functions: [fn],
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 401ff28fb..cb5515f73 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -903,7 +903,7 @@ export function isRefCounted(t: IrType): boolean {
export interface IrModule {
/** Bumped on any breaking IR change; serialize.ts refuses mismatches. */
- irVersion: 13;
+ irVersion: 14;
sourceFile: string;
functions: IrFunction[];
/** Class shapes. Constructors and methods are ordinary module functions
@@ -959,6 +959,8 @@ export interface IrModule {
unions?: IrUnionDef[];
/** Name of the synthetic function holding top-level statements. */
entry: string;
+ /** Executable roots run in isolated native worker contexts. */
+ workers?: boolean;
/** Outbound native FFI declarations used by `ffiCall` expressions.
* These are link-time C ABI imports, not runtime dynamic-library handles:
* executable builds resolve their symbols from the manifest's archive
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index f5c196dbc..6bba535b5 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -76,15 +76,18 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
fileHandle: false,
fetch: mod.embedded?.modules.some((m) => m.usesFetch === true) ?? false,
processEvents: false,
- emitter: (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
+ emitter:
+ mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
zlib: mod.embedded?.edges.some((e) => e.to === "node:zlib") ?? false,
dc: false,
assert: false,
- dynInvoke: false,
- dynAsync: mod.functions.some(
- (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
- ),
+ dynInvoke: mod.workers === true,
+ dynAsync:
+ mod.workers === true ||
+ mod.functions.some(
+ (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
+ ),
inspect: false,
childProcess: false,
net: false,
diff --git a/packages/compiler/src/ir/scalar-records.test.ts b/packages/compiler/src/ir/scalar-records.test.ts
index 1aef99c5c..5b632639b 100644
--- a/packages/compiler/src/ir/scalar-records.test.ts
+++ b/packages/compiler/src/ir/scalar-records.test.ts
@@ -40,7 +40,7 @@ const read = (localId: string, field = "a"): IrExpr => ({
function fixture(): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
records: [
diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts
index e77f2e6d7..cb5738135 100644
--- a/packages/compiler/src/ir/serialize.ts
+++ b/packages/compiler/src/ir/serialize.ts
@@ -2,9 +2,9 @@
* JSON's null/zero spellings must not erase NaN, infinities or negative zero. */
import type { IrModule } from "./ir.js";
-// Version 13 adds the NaN sentinel. Older readers interpret unknown tags
-// as negative infinity, so accepting a newer document would miscompile it.
-export const IR_VERSION = 13 as const;
+// Version 14 records worker execution, which requires thread-local program
+// state and matching runtime objects. Older readers must not erase that mode.
+export const IR_VERSION = 14 as const;
/** IR artifacts are plain data. Scan each function once before deciding
* whether its JSON encoding needs the complete-number-domain replacer. */
diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts
index 0d566b8b9..e6f240f87 100644
--- a/packages/compiler/src/ir/validate.test.ts
+++ b/packages/compiler/src/ir/validate.test.ts
@@ -315,7 +315,7 @@ function numericReadModule(overrides: Partial
...overrides,
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [
@@ -399,7 +399,7 @@ test.each(["sortPrimitive", "toSortedPrimitive"] as const)(
function expressionModule(expr: IrExpr, unions: IrUnionDef[]): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
unions,
@@ -1286,7 +1286,7 @@ test.each([
function tdzModule(mutable = true): IrModule {
const value: IrExpr = { kind: "numLit", value: 0, type: F64, loc };
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [
@@ -1454,7 +1454,7 @@ test("TDZ locals require a shared box", () => {
function discriminatedModule(): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index f64388bc8..b1814019a 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -134,7 +134,24 @@ export function validateModule(mod: IrModule): IrValidationError[] {
const ffiByName = new Map[number]>();
const ffiSymbols = new Set();
const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
+ if (mod.workers !== undefined && typeof mod.workers !== "boolean")
+ errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc });
+ if (mod.workers && (mod.lib !== undefined || mod.embedded !== undefined))
+ errors.push({
+ message: "worker execution requires a native static executable",
+ loc: moduleLoc,
+ });
for (const entry of mod.ffiImports ?? []) {
+ if (
+ mod.workers &&
+ entry.params.some(
+ (parameter) => isFfiCallbackParam(parameter) && parameter.callback.invoke === "foreign",
+ )
+ )
+ errors.push({
+ message: "worker execution cannot install foreign-thread callbacks",
+ loc: moduleLoc,
+ });
if (ffiByName.has(entry.name)) {
errors.push({ message: `duplicate FFI binding "${entry.name}"`, loc: moduleLoc });
}
@@ -968,6 +985,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
globalsById,
classValidation,
errors,
+ mod.workers === true,
);
}
return errors;
@@ -984,6 +1002,7 @@ function validateFunction(
globals: Map,
classValidation: ClassValidation,
errors: IrValidationError[],
+ workers: boolean,
): void {
const locals = new Map(fn.locals.map((l) => [l.id, l]));
const err = (message: string, loc: SrcLoc) =>
@@ -3711,6 +3730,15 @@ function validateFunction(
}
function checkLibCall(e: IrExpr & { kind: "libCall" }): void {
+ if (
+ e.fn === "atomics.op" &&
+ (e.args[0]?.type.kind !== "bytes" ||
+ !["u8", "i8", "u16", "i16", "u32", "i32"].includes(e.args[0].type.elem))
+ ) {
+ errors.push({ message: "integer Atomics require an integer typed array", loc: e.loc });
+ }
+ if (e.fn === "worker.new" && !workers)
+ errors.push({ message: "Worker construction requires worker execution mode", loc: e.loc });
const sig = LIB_FN_SIGS[e.fn];
if (!sig) {
err(`libCall of unknown library function "${e.fn as string}"`, e.loc);
diff --git a/packages/compiler/src/library/fence-eval.test.ts b/packages/compiler/src/library/fence-eval.test.ts
index 5735c858e..cbffca9c7 100644
--- a/packages/compiler/src/library/fence-eval.test.ts
+++ b/packages/compiler/src/library/fence-eval.test.ts
@@ -15,7 +15,7 @@ function program(member: string): IrModule {
loc,
};
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "probe",
functions: [
diff --git a/packages/compiler/src/library/int-infer.test.ts b/packages/compiler/src/library/int-infer.test.ts
index 8431b999a..d06dd32cd 100644
--- a/packages/compiler/src/library/int-infer.test.ts
+++ b/packages/compiler/src/library/int-infer.test.ts
@@ -97,7 +97,7 @@ const sink = (name: string): IrFunction => ({
/** A module holding the case function plus the two declared sinks. */
function caseModule(params: string[], locals: string[], body: IrStmt[]): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "corpus.ts",
functions: [
sink("send"),
@@ -196,7 +196,7 @@ const RECORD_CFG: IntSlotConfig = {
function recordCase(body: IrStmt[], names = ["m"], extraFns: IrFunction[] = []): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "fields.ts",
functions: [
...extraFns,
@@ -237,7 +237,7 @@ const classCountRead = (): IrExpr => ({
function onlyOrdinaryClass(body: IrStmt[]): IntVerdict {
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "class-fields.ts",
functions: [
sink("send"),
@@ -581,7 +581,7 @@ describe("the domain's edges beyond the corpus", () => {
loc,
};
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "optional.ts",
functions: [
{
diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts
index 3afa36843..dd3299bef 100644
--- a/packages/compiler/src/library/ir-policy.ts
+++ b/packages/compiler/src/library/ir-policy.ts
@@ -59,6 +59,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["als.", "AsyncLocalStorage"],
["urj.", "unhandled-rejection tracking"],
["dc.", "the diagnostics_channel surface"],
+ ["worker.new", "native worker threads"],
];
/** Value/type kinds whose mere presence means an excluded unit's code (or
@@ -131,6 +132,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr
// fine-grained table missed must still refuse.
const features = moduleRuntimeFeatures(mod);
const coarse: [boolean, string][] = [
+ [mod.workers === true, "native worker threads"],
[features.processEvents, "process signal/exit listeners or the stdin event surface"],
[features.net, "the node:net surface"],
[features.http, "the node:http surface"],
diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts
index 7d3c1be53..4e98e6d94 100644
--- a/packages/compiler/src/library/library-cache.test.ts
+++ b/packages/compiler/src/library/library-cache.test.ts
@@ -261,7 +261,7 @@ test("semantic library cache restores and rebases IR after a comment-only edit",
const sourceBefore = await readFile(f.source, "utf8");
const returnStart = sourceBefore.indexOf("return");
const semanticMod = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: f.source,
functions: [
{
@@ -324,7 +324,7 @@ test("semantic library cache refuses token and directive edits", async () => {
const f = await fixture();
const sourceBefore = await readFile(f.source, "utf8");
const semanticMod = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: f.source,
functions: [
{
@@ -372,7 +372,7 @@ test("semantic LLVM cache accepts comment edits that shift source lines", async
const f = await fixture();
const sourceBefore = await readFile(f.source, "utf8");
const semanticMod = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: f.source,
functions: [
{
@@ -447,7 +447,7 @@ test("semantic LLVM cache accepts line separator normalization", async () => {
frontend: tracker.snapshot(),
semantic: {
mod: {
- irVersion: 13,
+ irVersion: 14,
sourceFile: f.source,
functions: [],
entry: "__main",
@@ -468,7 +468,7 @@ test("semantic LLVM cache rebases comment edits in multi-source graphs", async (
const importedSource = "export function helper(): number { return 1; }\n";
await writeFile(imported, importedSource);
const semanticMod = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: f.source,
functions: [
{
diff --git a/packages/compiler/src/native/compiler.test.ts b/packages/compiler/src/native/compiler.test.ts
index 61909f139..7adde68a2 100644
--- a/packages/compiler/src/native/compiler.test.ts
+++ b/packages/compiler/src/native/compiler.test.ts
@@ -60,7 +60,7 @@ test.each([
writeFileSync(output, "previous executable");
writeFileSync(llvmPath, "previous LLVM");
const module: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: entry,
entry: "%main",
functions: [
@@ -197,7 +197,7 @@ test.each([false, true])(
writeFileSync(output, "previous object");
writeFileSync(llvmPath, "previous LLVM");
const module: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: entry,
entry: "%main",
functions: [
diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts
index feb70316e..9e832d58d 100644
--- a/packages/compiler/src/native/prepare.ts
+++ b/packages/compiler/src/native/prepare.ts
@@ -60,6 +60,7 @@ function implementationIdentity(toolchain: NativeToolchain): string {
function validFeatures(value: unknown): value is NativeLinkFeatures {
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
const features = value as Record;
+ if (features["workers"] !== undefined && typeof features["workers"] !== "boolean") return false;
return [
"dynamic",
"regex",
diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts
index 299f9d6e0..e3cdee693 100644
--- a/packages/compiler/src/native/sanitizer.test.ts
+++ b/packages/compiler/src/native/sanitizer.test.ts
@@ -72,6 +72,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
archives: [artifact],
systemLibraries: [],
features: {
+ workers: false,
dynamic: false,
regex: true,
copying: false,
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index 1d44f9361..193ac7814 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -1704,7 +1704,7 @@
"kind": "node-builtin",
"name": "process.getBuiltinModule",
"status": "static",
- "note": "native path and os export subsets plus main-thread worker_threads metadata; other modules and exports throw SC2020"
+ "note": "native path and os export subsets plus current-thread worker_threads metadata; other modules and exports throw SC2020"
},
{
"id": "node-builtin.process.getgid",
@@ -2233,6 +2233,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
+ {
+ "id": "node-builtin.worker_threads.Worker",
+ "kind": "node-builtin",
+ "name": "worker_threads.Worker",
+ "status": "static",
+ "note": "native threads with statically compiled entry points, workerData, argv and message events"
+ },
{
"id": "node-builtin.worker_threads.isMainThread",
"kind": "node-builtin",
@@ -2240,6 +2247,13 @@
"status": "static",
"note": "constant value read"
},
+ {
+ "id": "node-builtin.worker_threads.metadata",
+ "kind": "node-builtin",
+ "name": "worker_threads metadata",
+ "status": "static",
+ "note": "current-thread metadata and the worker's parent message port"
+ },
{
"id": "node-builtin.worker_threads.threadId",
"kind": "node-builtin",
@@ -2639,6 +2653,13 @@
"name": "Array.prototype.with",
"status": "static"
},
+ {
+ "id": "stdlib.atomics",
+ "kind": "stdlib",
+ "name": "Atomics",
+ "status": "static",
+ "note": "8-, 16- and 32-bit integer operations, plus Int32Array wait and notify"
+ },
{
"id": "stdlib.date.UTC",
"kind": "stdlib",
@@ -3994,6 +4015,13 @@
"status": "static",
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
},
+ {
+ "id": "stdlib.sharedArrayBuffer",
+ "kind": "stdlib",
+ "name": "SharedArrayBuffer",
+ "status": "static",
+ "note": "fixed shared storage with context-local typed-array and DataView wrappers"
+ },
{
"id": "stdlib.string.at",
"kind": "stdlib",
diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts
index 6714fcbaf..9c40b0aeb 100644
--- a/packages/compiler/test/bytes-element-emission.test.ts
+++ b/packages/compiler/test/bytes-element-emission.test.ts
@@ -75,7 +75,7 @@ function fixture(): IrModule {
);
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "__main",
functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }],
@@ -145,7 +145,7 @@ function receiverReassignmentFixture(): IrModule {
];
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "__main",
functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }],
@@ -212,7 +212,7 @@ function integerLoopFixture(mutatesIndex = false): IrModule {
{ kind: "bytesSet", arr: bytesRef(), index: indexRef(), value: ref("sum"), loc },
);
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "__main",
functions: [
@@ -271,7 +271,7 @@ test("LLVM emission performs typed-array element access directly on the valid pa
const mod = fixture();
expect(validateModule(mod)).toEqual([]);
const ll = emitLlvmModule(mod);
- expect(ll).toContain("%ScrBytes = type { i64, i64, i32, ptr, ptr, i8 }");
+ expect(ll).toContain("%ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, ptr }");
expect(ll).toContain("getelementptr inbounds i8");
expect(ll).toContain("getelementptr inbounds i32");
expect(ll).toContain("getelementptr inbounds float");
@@ -333,7 +333,7 @@ test("large record clones stay outlined while small clones remain inlineable", (
const fields = Array.from({ length: count }, (_, i) => ({ name: `f${i}`, type: F64 }));
const type = { kind: "record", shapeId: id } as const;
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "record-clone.ts",
entry: "__main",
records: [{ id, fields }],
diff --git a/packages/compiler/test/emit-llvm.test.ts b/packages/compiler/test/emit-llvm.test.ts
index 226f7eebf..67f6ef798 100644
--- a/packages/compiler/test/emit-llvm.test.ts
+++ b/packages/compiler/test/emit-llvm.test.ts
@@ -35,7 +35,7 @@ test("strings: literals, concat in a loop, toString, RC-clean under audit", asyn
// while (i < 3) { acc = acc + ("-" + i); i = i + 1; }
// console.log(acc, acc === "x-0-1-2", "α∂" < "β");
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "s.ts",
entry: "__main",
functions: [
@@ -137,7 +137,7 @@ test("short-circuit: right operand of && only evaluates when left is true", asyn
// if (false && sideEffect()) {} ; if (true || sideEffect()) {}
// console.log("done")
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "l.ts",
entry: "__main",
functions: [
@@ -227,7 +227,7 @@ test("string params: callee owns and releases; returns transfer ownership", asyn
// function greet(who: string): string { return "hi " + who; }
// console.log(greet("world"));
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "p.ts",
entry: "__main",
functions: [
diff --git a/packages/compiler/test/fixtures/fib-ir.ts b/packages/compiler/test/fixtures/fib-ir.ts
index e33f2f7df..f1a4a4218 100644
--- a/packages/compiler/test/fixtures/fib-ir.ts
+++ b/packages/compiler/test/fixtures/fib-ir.ts
@@ -17,7 +17,7 @@ const n = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, lo
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
export const fibModule: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "fib.ts",
entry: "__main",
functions: [
diff --git a/packages/compiler/test/fromcharcode-emission.test.ts b/packages/compiler/test/fromcharcode-emission.test.ts
index dfddc0db2..4ddccbe67 100644
--- a/packages/compiler/test/fromcharcode-emission.test.ts
+++ b/packages/compiler/test/fromcharcode-emission.test.ts
@@ -7,7 +7,7 @@ test("scalar fromCharCode avoids a heap argument pack in LLVM", () => {
const loc = { file: "scalar-char.ts", start: 0, end: 0 };
const value: IrExpr = { kind: "numLit", value: 65, type: F64, loc };
const mod: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "__main",
globals: [],
diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts
index 40b18b908..e8e49128a 100644
--- a/packages/compiler/test/ir.test.ts
+++ b/packages/compiler/test/ir.test.ts
@@ -22,7 +22,7 @@ test("fib module JSON round-trips", () => {
test("validator rejects type mismatches and bad references", () => {
const loc = { file: "t.ts", start: 0, end: 0 };
const bad: IrModule = {
- irVersion: 13,
+ irVersion: 14,
sourceFile: "t.ts",
entry: "__main",
functions: [
@@ -108,7 +108,7 @@ test("serializer round-trips ±Infinity and NaN", () => {
});
test("deserializer rejects the previous IR version", () => {
- const json = serializeModule(fibModule).replace('"irVersion": 13', '"irVersion": 12');
+ const json = serializeModule(fibModule).replace('"irVersion": 14', '"irVersion": 13');
expect(() => deserializeModule(json)).toThrow(/version mismatch/);
});
diff --git a/packages/compiler/test/llvm-lib-dispatch.test.ts b/packages/compiler/test/llvm-lib-dispatch.test.ts
index 5cd8d3853..a92e93948 100644
--- a/packages/compiler/test/llvm-lib-dispatch.test.ts
+++ b/packages/compiler/test/llvm-lib-dispatch.test.ts
@@ -19,7 +19,7 @@ const dyn = (): IrExpr => ({ kind: "dynFrom", value: str("value"), type: DYN, lo
function libCallModule(fn: IrLibFn, args: IrExpr[], type: IrType): IrModule {
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "__main",
functions: [
diff --git a/packages/compiler/test/llvm-runtime-abi.test.ts b/packages/compiler/test/llvm-runtime-abi.test.ts
index 63a609386..7bec5f72d 100644
--- a/packages/compiler/test/llvm-runtime-abi.test.ts
+++ b/packages/compiler/test/llvm-runtime-abi.test.ts
@@ -61,6 +61,8 @@ const EMIT_FIXTURES = [
"996-fs-rc-stress.ts",
"997-fs-modules/main.ts",
"1006-json-fs-config.ts",
+ "native-worker-roots/main.ts",
+ "native-worker-shared-views.ts",
];
interface CProto {
@@ -158,6 +160,7 @@ function splitParams(argsText: string): string[] {
async function parseHeader(): Promise<{ protos: Map; dataSyms: Set }> {
const raw = await readFile(headerPath, "utf8");
const src = raw
+ .replace(/\\\r?\n/g, "")
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/\/\/[^\n]*/g, " ")
.replace(/^[ \t]*#[^\n]*$/gm, " ");
@@ -317,19 +320,22 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => {
#include
#include "scr_runtime.h"
-/* LLVM emits: %ScrBytes = type { i64, i64, i32, ptr, ptr, i8 }.
+/* LLVM emits: %ScrBytes = type { i64, i64, i32, ptr, ptr, i8, i8, i8, ptr }.
* Keep every ABI fact used by its field-index GEPs explicit here. */
_Static_assert(sizeof(size_t) == 8, "LLVM ScrBytes expects 64-bit size_t");
_Static_assert(sizeof(void *) == 8, "LLVM ScrBytes expects 64-bit pointers");
_Static_assert(sizeof(ScrBytesElem) == 4, "LLVM ScrBytes elem field is i32");
_Static_assert(_Alignof(ScrBytes) == 8, "LLVM ScrBytes alignment changed");
-_Static_assert(sizeof(ScrBytes) == 48, "LLVM ScrBytes size changed");
+_Static_assert(sizeof(ScrBytes) == 56, "LLVM ScrBytes size changed");
_Static_assert(offsetof(ScrBytes, rc) == 0, "LLVM ScrBytes.rc offset changed");
_Static_assert(offsetof(ScrBytes, len) == 8, "LLVM ScrBytes.len offset changed");
_Static_assert(offsetof(ScrBytes, elem) == 16, "LLVM ScrBytes.elem offset changed");
_Static_assert(offsetof(ScrBytes, data) == 24, "LLVM ScrBytes.data offset changed");
_Static_assert(offsetof(ScrBytes, backing) == 32, "LLVM ScrBytes.backing offset changed");
_Static_assert(offsetof(ScrBytes, is_buffer) == 40, "LLVM ScrBytes.is_buffer offset changed");
+_Static_assert(offsetof(ScrBytes, is_data_view) == 41, "LLVM ScrBytes.is_data_view offset changed");
+_Static_assert(offsetof(ScrBytes, external) == 42, "LLVM ScrBytes.external offset changed");
+_Static_assert(offsetof(ScrBytes, shared) == 48, "LLVM ScrBytes.shared offset changed");
`,
);
const driver = resolveCc();
diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts
index 3056e5aa7..860d9d9a3 100644
--- a/packages/compiler/test/native-codegen-integration.test.ts
+++ b/packages/compiler/test/native-codegen-integration.test.ts
@@ -312,7 +312,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
- expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v6");
+ expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v7");
const clangExe = join(dir, "clang-program");
const linkDriver = join(dir, "clang-driver.c");
@@ -646,7 +646,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
.split("\n");
expect(undefinedSymbols).toContain("_sf_scale");
expect(undefinedSymbols).toContain("_sf_callback_mix");
- expect(undefinedSymbols).toContain("_scr_runtime_abi_v6");
+ expect(undefinedSymbols).toContain("_scr_runtime_abi_v7");
});
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
diff --git a/packages/compiler/test/string-accumulation-emission.test.ts b/packages/compiler/test/string-accumulation-emission.test.ts
index 3cfda1c74..5e1f40099 100644
--- a/packages/compiler/test/string-accumulation-emission.test.ts
+++ b/packages/compiler/test/string-accumulation-emission.test.ts
@@ -86,7 +86,7 @@ function fixture(): IrModule {
.find((fn) => fn.name === "negative")!
.body.splice(1, 0, { kind: "varDecl", localId: "other", init: str("other"), loc });
return {
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "__main",
globals: [{ id: "%g.e.acc", name: "globalAccumulator", type: STRING, mutable: true }],
diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json
index b1e45c223..1c25a7273 100644
--- a/packages/compiler/test/ts7/baselines/order-parity.json
+++ b/packages/compiler/test/ts7/baselines/order-parity.json
@@ -14517,6 +14517,126 @@
"/tests/corpus/scalar-string-slices.ts"
],
"diags": []
+ },
+ "/tests/corpus/atomics-integer-operations.ts": {
+ "order": [
+ "/tests/corpus/atomics-integer-operations.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-buffer-consumers.ts": {
+ "order": [
+ "/tests/corpus/native-worker-buffer-consumers.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-clone-errors.ts": {
+ "order": [
+ "/tests/corpus/native-worker-clone-errors.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-commonjs.cjs": {
+ "order": [
+ "/tests/corpus/native-worker-commonjs.cjs"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-delayed-messages.ts": {
+ "order": [
+ "/tests/corpus/native-worker-delayed-messages.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-environment.ts": {
+ "order": [
+ "/tests/corpus/native-worker-environment.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-error.ts": {
+ "order": [
+ "/tests/corpus/native-worker-error.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-exit-listeners.ts": {
+ "order": [
+ "/tests/corpus/native-worker-exit-listeners.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-io.ts": {
+ "order": [
+ "/tests/corpus/native-worker-io.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-loop-cancellation.ts": {
+ "order": [
+ "/tests/corpus/native-worker-loop-cancellation.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-message-graphs.ts": {
+ "order": [
+ "/tests/corpus/native-worker-message-graphs.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-messages.ts": {
+ "order": [
+ "/tests/corpus/native-worker-messages.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-port-lifetime.ts": {
+ "order": [
+ "/tests/corpus/native-worker-port-lifetime.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-process-exit.ts": {
+ "order": [
+ "/tests/corpus/native-worker-process-exit.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-references.ts": {
+ "order": [
+ "/tests/corpus/native-worker-references.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-roots/main.ts": {
+ "order": [
+ "/tests/corpus/native-worker-roots/main.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-shared-views.ts": {
+ "order": [
+ "/tests/corpus/native-worker-shared-views.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-terminate-loop.ts": {
+ "order": [
+ "/tests/corpus/native-worker-terminate-loop.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-terminate-suspended.ts": {
+ "order": [
+ "/tests/corpus/native-worker-terminate-suspended.ts"
+ ],
+ "diags": []
+ },
+ "/tests/corpus/native-worker-terminate-wait.ts": {
+ "order": [
+ "/tests/corpus/native-worker-terminate-wait.ts"
+ ],
+ "diags": []
}
}
}
diff --git a/packages/compiler/test/ts7/order-parity.test.ts b/packages/compiler/test/ts7/order-parity.test.ts
index 1ae83323f..3f888c5f0 100644
--- a/packages/compiler/test/ts7/order-parity.test.ts
+++ b/packages/compiler/test/ts7/order-parity.test.ts
@@ -38,7 +38,7 @@
* sweep and the upgrade playbook's step 2. */
import { execFile } from "node:child_process";
-import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
@@ -47,6 +47,7 @@ import { afterAll, describe, expect, test } from "vitest";
import { checkPreflightTs7 } from "../../src/frontend/program-node.js";
import { Ts7Host } from "../../src/frontend/ts7/program-adapter.js";
import type { ScrDiagnostic } from "../../src/diagnostics/diagnostic.js";
+import { allOrderEntries, orderEntriesUnder } from "../../../../scripts/ts7-order-fixtures.mjs";
const repoRoot = join(import.meta.dirname, "../../../..");
const baselinePath = join(import.meta.dirname, "baselines/order-parity.json");
@@ -111,25 +112,13 @@ async function nativeAnswers(entries: string[]): Promise {
}
function entriesUnder(dir: string): string[] {
- const exts = ["ts", "js", "mjs", "cjs"];
- return exts
- .flatMap((ext) => [
- ...globSync(join(repoRoot, dir, `*.${ext}`)),
- ...globSync(join(repoRoot, dir, `*/main.${ext}`)),
- ])
- .sort();
+ return orderEntriesUnder(repoRoot, dir);
}
const FULL = process.env["SCRIPTC_TS7_ALL"] === "1" || UPDATE;
function allEntries(): string[] {
- return [
- ...entriesUnder("tests/corpus"),
- ...entriesUnder("tests/diagnostics"),
- ...entriesUnder("tests/fixtures/npm/cases"),
- ...globSync(join(repoRoot, "tests/fixtures/strictness/*/main.ts")).sort(),
- ...globSync(join(repoRoot, "tests/fixtures/node-types/*.ts")).sort(),
- ];
+ return allOrderEntries(repoRoot);
}
function pickEntries(): string[] {
diff --git a/packages/compiler/test/worker-threads.test.ts b/packages/compiler/test/worker-threads.test.ts
new file mode 100644
index 000000000..503ff01b9
--- /dev/null
+++ b/packages/compiler/test/worker-threads.test.ts
@@ -0,0 +1,156 @@
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, expect, test, vi } from "vitest";
+import { compile, compileLibrary } from "../src/index.js";
+
+const directories: string[] = [];
+afterEach(async () => {
+ vi.unstubAllEnvs();
+ await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
+});
+
+async function fixture(source: string) {
+ const outDir = await mkdtemp(join(tmpdir(), "scriptc-worker-target-"));
+ directories.push(outDir);
+ const entry = join(outDir, "main.ts");
+ await writeFile(entry, source);
+ return { entry, outDir, outPath: join(outDir, "program.ll"), outputKind: "llvm" as const };
+}
+const worker =
+ 'import { Worker } from "node:worker_threads"; new Worker(new URL(import.meta.url));';
+
+test.each(["dynamic", "wasi"])(
+ "worker constructors refuse %s execution before code generation",
+ async (mode) => {
+ const request = await fixture(worker);
+ if (mode === "wasi") {
+ vi.stubEnv("SCRIPTC_CC", "zigcc");
+ vi.stubEnv("SCRIPTC_TARGET", "wasm32-wasi");
+ }
+ const result = await compile(request.entry, { ...request, dynamic: mode === "dynamic" });
+ expect(result.ok).toBe(false);
+ if (!result.ok)
+ expect(result.diagnostics).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ code: "SC2020",
+ message: expect.stringContaining("Worker execution"),
+ }),
+ ]),
+ );
+ },
+);
+
+test("runtime worker filenames produce an actionable source diagnostic", async () => {
+ const request = await fixture(
+ 'import { Worker } from "node:worker_threads"; new Worker(process.argv[2]);',
+ );
+ const result = await compile(request.entry, request);
+ expect(result.ok).toBe(false);
+ if (!result.ok)
+ expect(result.diagnostics).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ code: "SC2020",
+ hint: expect.stringContaining("statically compiled"),
+ }),
+ ]),
+ );
+});
+
+test("libraries refuse worker roots through the library diagnostic boundary", async () => {
+ const request = await fixture(worker + "\nexport function boot(): number { return 0; }");
+ const profilePath = join(request.outDir, "profile.json");
+ await writeFile(
+ profilePath,
+ JSON.stringify({
+ profile_format: 1,
+ name: "worker-library",
+ entry: "main.ts",
+ emission: "llvm",
+ abi: {
+ prefix: "w_",
+ init_symbol: "w_init",
+ sink_register_symbol: "w_panic",
+ collect_symbol: null,
+ result_reset_symbol: null,
+ },
+ exports: [{ export: "boot", symbol: "w_boot", params: [], returns: "f64" }],
+ }),
+ );
+ const result = await compileLibrary({ profilePath, outDir: request.outDir });
+ expect(result.ok).toBe(false);
+ if (!result.ok)
+ expect(result.diagnostics).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ code: "SC4005", message: expect.stringContaining("worker") }),
+ ]),
+ );
+});
+
+test("worker TLS arguments use local snapshots before native pointer expansion", async () => {
+ const request = await fixture(
+ worker +
+ '\nimport { createSecureContext } from "node:tls"; const cert = Buffer.from(new SharedArrayBuffer(4)); createSecureContext({cert, key: cert});',
+ );
+ const result = await compile(request.entry, request);
+ expect(result.ok, JSON.stringify(result)).toBe(true);
+ if (result.ok) {
+ const llvm = await readFile(request.outPath, "utf8");
+ expect(llvm).toContain("call ptr @scr_bytes_local_copy(");
+ expect(llvm).toContain("call ptr @scr_tls_create_secure_context(");
+ }
+});
+
+test("worker programs refuse foreign callbacks and guard byte-pointer FFI calls", async () => {
+ for (const foreign of [true, false]) {
+ const source = foreign
+ ? "declare function nativeStart(cb: (value: number) => void): void; nativeStart(() => {});"
+ : "declare function consume(value: Uint8Array): number; consume(new Uint8Array(new SharedArrayBuffer(4)));";
+ const request = await fixture(worker + source);
+ const ffiProfilePath = join(request.outDir, "ffi.json");
+ await writeFile(
+ ffiProfilePath,
+ JSON.stringify({
+ ffi_format: 5,
+ functions: [
+ foreign
+ ? {
+ name: "nativeStart",
+ symbol: "native_start",
+ returns: "void",
+ params: [
+ {
+ callback: {
+ id: "tick",
+ params: ["f64", { context: "tick" }],
+ returns: "void",
+ lifetime: "retained",
+ invoke: "foreign",
+ },
+ },
+ { context: "tick" },
+ ],
+ }
+ : { name: "consume", symbol: "consume", params: ["bytes"], returns: "f64" },
+ ],
+ }),
+ );
+ const result = await compile(request.entry, { ...request, ffiProfilePath });
+ expect(result.ok, JSON.stringify(result)).toBe(!foreign);
+ if (result.ok)
+ expect(await readFile(request.outPath, "utf8")).toContain(
+ "call void @scr_bytes_require_unshared(",
+ );
+ else
+ expect(result.diagnostics).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ code: "SC2020",
+ message: expect.stringContaining("foreign-thread"),
+ }),
+ ]),
+ );
+ }
+});
diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs
index 2b1205130..caec30cc4 100644
--- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs
+++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs
@@ -1,3 +1,4 @@
+import { withWorkerRuntimeVariants } from "../runtime-pack-common/worker-matrix.mjs";
import { withLibraryRuntimeFlavors } from "../runtime-pack-common/library-matrix.mjs";
/**
@@ -11,6 +12,8 @@ const any = (...features) => ({ any: features });
const all = (...features) => ({ all: features });
const BASE_RUNTIME_SOURCES = [
+ "scr_context.c",
+ "scr_shared.c",
"scr_number.c",
"scr_bigint.c",
"scr_string.c",
@@ -119,29 +122,31 @@ function variantsFor(source) {
return variants;
}
-export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors({
- schema: "scriptc.runtime-pack-matrix.v1",
- target: {
- name: "macos-arm64",
- llvm_triple: "arm64-apple-macosx14.0.0",
- architecture: "arm64",
- object_format: "macho",
- minimum_os: "14.0",
- },
- flavors: { release: { optimization: "-O2" }, dev: { optimization: "-O0" } },
- executable_section_elimination: EXECUTABLE_SECTION_ELIMINATION,
- runtime_units: [
- ...BASE_RUNTIME_SOURCES.map((source) => ({ source, predicate: true })),
- ...optional.map(([source, predicate]) => ({ source, predicate })),
- ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })),
- archives: [
- { id: "quickjs", predicate: "dynamic" },
- { id: "libregexp", predicate: { all: ["regex"], not: ["dynamic"] } },
- { id: "zlib", predicate: "zlibEffective" },
- { id: "mbedtls", predicate: "tlsEffective" },
- ],
- system_libraries: [
- { name: "System", predicate: true },
- { name: "m", predicate: "dynamic" },
- ],
-});
+export const RUNTIME_PACK_MATRIX = withLibraryRuntimeFlavors(
+ withWorkerRuntimeVariants({
+ schema: "scriptc.runtime-pack-matrix.v1",
+ target: {
+ name: "macos-arm64",
+ llvm_triple: "arm64-apple-macosx14.0.0",
+ architecture: "arm64",
+ object_format: "macho",
+ minimum_os: "14.0",
+ },
+ flavors: { release: { optimization: "-O2" }, dev: { optimization: "-O0" } },
+ executable_section_elimination: EXECUTABLE_SECTION_ELIMINATION,
+ runtime_units: [
+ ...BASE_RUNTIME_SOURCES.map((source) => ({ source, predicate: true })),
+ ...optional.map(([source, predicate]) => ({ source, predicate })),
+ ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })),
+ archives: [
+ { id: "quickjs", predicate: "dynamic" },
+ { id: "libregexp", predicate: { all: ["regex"], not: ["dynamic"] } },
+ { id: "zlib", predicate: "zlibEffective" },
+ { id: "mbedtls", predicate: "tlsEffective" },
+ ],
+ system_libraries: [
+ { name: "System", predicate: true },
+ { name: "m", predicate: "dynamic" },
+ ],
+ }),
+);
diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs
index 31714c5d0..0812c1345 100644
--- a/packages/runtime-darwin-arm64/scripts/build.mjs
+++ b/packages/runtime-darwin-arm64/scripts/build.mjs
@@ -239,7 +239,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: RUNTIME_PACK_MATRIX.target,
- runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" },
+ runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" },
compiler: {
command: compiler,
identity: compilerVersion,
diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs
index eaf05b53b..d4728dc50 100644
--- a/packages/runtime-pack-common/library-matrix.mjs
+++ b/packages/runtime-pack-common/library-matrix.mjs
@@ -26,7 +26,10 @@ export function withLibraryRuntimeFlavors(matrix) {
},
].map((unit) => ({
...unit,
- variants: unit.variants.filter((variant) => !variant.defines.includes("SCR_DYNAMIC")),
+ variants: unit.variants.filter(
+ (variant) =>
+ !variant.defines.includes("SCR_DYNAMIC") && !variant.defines.includes("SCR_WORKERS"),
+ ),
}));
const flavors = { ...matrix.flavors };
for (const [flavor, spec] of Object.entries(matrix.flavors)) {
diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs
index ffe13d852..27eb37687 100644
--- a/packages/runtime-pack-common/runtime-pack-matrix.mjs
+++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs
@@ -1,3 +1,4 @@
+import { withWorkerRuntimeVariants } from "./worker-matrix.mjs";
import { withLibraryRuntimeFlavors } from "./library-matrix.mjs";
/**
@@ -9,6 +10,8 @@ const any = (...features) => ({ any: features });
const all = (...features) => ({ all: features });
const BASE_RUNTIME_SOURCES = [
+ "scr_context.c",
+ "scr_shared.c",
"scr_number.c",
"scr_bigint.c",
"scr_string.c",
@@ -125,23 +128,25 @@ export function createRuntimePackMatrix({
{ id: "zlib", predicate: "zlibEffective" },
{ id: "mbedtls", predicate: "tlsEffective" },
];
- const matrix = withLibraryRuntimeFlavors({
- schema: "scriptc.runtime-pack-matrix.v1",
- target,
- flavors: { release: { optimization: "-O2" }, dev: { optimization: "-O0" } },
- executable_section_elimination: { compile_flags: compileFlags },
- runtime_units: [
- ...BASE_RUNTIME_SOURCES.filter((source) => !omitRuntimeSources.includes(source)).map(
- (source) => ({ source, predicate: true }),
- ),
- ...extraRuntimeSources.map((source) => ({ source, predicate: true })),
- ...OPTIONAL.filter(([source]) => !omitOptionalSources.includes(source)).map(
- ([source, predicate]) => ({ source, predicate }),
- ),
- ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })),
- archives: vendorArchives.filter(({ id }) => !omitArchives.includes(id)),
- system_libraries: systemLibraries,
- });
+ const matrix = withLibraryRuntimeFlavors(
+ withWorkerRuntimeVariants({
+ schema: "scriptc.runtime-pack-matrix.v1",
+ target,
+ flavors: { release: { optimization: "-O2" }, dev: { optimization: "-O0" } },
+ executable_section_elimination: { compile_flags: compileFlags },
+ runtime_units: [
+ ...BASE_RUNTIME_SOURCES.filter((source) => !omitRuntimeSources.includes(source)).map(
+ (source) => ({ source, predicate: true }),
+ ),
+ ...extraRuntimeSources.map((source) => ({ source, predicate: true })),
+ ...OPTIONAL.filter(([source]) => !omitOptionalSources.includes(source)).map(
+ ([source, predicate]) => ({ source, predicate }),
+ ),
+ ].map((unit) => ({ ...unit, variants: variantsFor(unit.source) })),
+ archives: vendorArchives.filter(({ id }) => !omitArchives.includes(id)),
+ system_libraries: systemLibraries,
+ }),
+ );
if (libraryOnly) {
matrix.flavors = Object.fromEntries(
Object.entries(matrix.flavors).filter(([name]) => name.startsWith("library-")),
diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs
index 0b56ef3ad..c783c1a20 100644
--- a/packages/runtime-pack-common/scripts/build.mjs
+++ b/packages/runtime-pack-common/scripts/build.mjs
@@ -293,7 +293,7 @@ async function build() {
package: packageManifest.name,
version: packageManifest.version,
target: matrix.target,
- runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" },
+ runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" },
compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple },
macros: {
executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"],
diff --git a/packages/runtime-pack-common/worker-matrix.mjs b/packages/runtime-pack-common/worker-matrix.mjs
new file mode 100644
index 000000000..8925c0815
--- /dev/null
+++ b/packages/runtime-pack-common/worker-matrix.mjs
@@ -0,0 +1,33 @@
+/** Worker executables use the same runtime ABI with context-local state.
+ * Every selected runtime object must agree on TLS, including optional units.
+ * Ordinary and library executables retain their existing object variants. */
+export function withWorkerRuntimeVariants(matrix) {
+ if (matrix.target.object_format === "wasm") return matrix;
+ const sources = ["scr_worker.c", "scr_worker_events.c", "scr_mailbox.c", "scr_message.c"];
+ return {
+ ...matrix,
+ runtime_units: [
+ ...matrix.runtime_units.map((unit) => ({
+ ...unit,
+ variants: unit.variants.flatMap((variant) => [
+ { ...variant, when: { ...variant.when, workers: false } },
+ ...(variant.defines.includes("SCR_DYNAMIC")
+ ? []
+ : [
+ {
+ ...variant,
+ id: `${variant.id}-workers`,
+ when: { ...variant.when, workers: true },
+ defines: [...variant.defines, "SCR_WORKERS"],
+ },
+ ]),
+ ]),
+ })),
+ ...sources.map((source) => ({
+ source,
+ predicate: "workers",
+ variants: [{ id: "workers", when: { workers: true }, defines: ["SCR_WORKERS"] }],
+ })),
+ ],
+ };
+}
diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c
index 225d2d589..2b62a5927 100644
--- a/packages/runtime/src/scr_assert.c
+++ b/packages/runtime/src/scr_assert.c
@@ -361,6 +361,7 @@ void scr_assert_deep_result(bool equal, bool negated, ScrStr *msg, bool has_msg)
* by construction (the frontend's same-static-type gate), but the elem
* check stays: a defensive answer beats a miscompare. */
static bool scr_assert_bytes_content_eq(const ScrBytes *a, const ScrBytes *b) {
+ SCR_SHARED_GUARD(a, b);
if (a->elem != b->elem || a->len != b->len) return false;
size_t nbytes = a->len * scr_bytes_elem_size(a->elem);
return nbytes == 0 || memcmp(a->data, b->data, nbytes) == 0;
@@ -542,8 +543,7 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) {
if (a->buffer != b->buffer) return false; /* the prototype gate */
const ScrBytes *x = a->v.bytes, *y = b->v.bytes;
if (x->elem != y->elem || x->len != y->len) return false;
- size_t nbytes = x->len * scr_bytes_elem_size(x->elem);
- return nbytes == 0 || memcmp(x->data, y->data, nbytes) == 0;
+ return scr_assert_bytes_content_eq(x, y);
}
case SCR_DYN_ARR: {
if (a->v.arr.len != b->v.arr.len) return false;
@@ -712,7 +712,7 @@ static void scr_assert_cf_value(ScrAssertBuf *b, const ScrDyn *d, size_t indent,
ab_char(b, '\n');
scr_assert_cf_pad(b, indent + 2);
char tmp[40];
- size_t n = scr_assert_inspect_f64((double)((const unsigned char *)bs->data)[i], tmp);
+ size_t n = scr_assert_inspect_f64(scr_bytes_get(bs, (double)i), tmp);
ab_bytes(b, tmp, n);
if (i + 1 < bs->len) ab_char(b, ',');
}
diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c
index d416db601..8a9f5c3bd 100644
--- a/packages/runtime/src/scr_async.c
+++ b/packages/runtime/src/scr_async.c
@@ -197,7 +197,7 @@ struct ScrPromise {
};
#ifdef SCR_RC_AUDIT
-static long scr_live_promises = 0;
+static SCR_TL long scr_live_promises = 0;
long scr_promise_live_count(void) { return scr_live_promises; }
#endif
@@ -296,7 +296,7 @@ bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b) {
* registration — the scr_urj_deliver_fn pattern, so listener-free
* binaries keep their size class): called when a promise the checkpoint
* report already delivered as unhandled gains a handler. */
-void (*scr_rjh_notify_fn)(ScrPromise *p) = NULL;
+SCR_TL void (*scr_rjh_notify_fn)(ScrPromise *p) = NULL;
/* Every handler attach funnels here: mark the rejection observed, and
* fire Node's 'rejectionHandled' when the attach arrived AFTER the
@@ -383,6 +383,11 @@ typedef ucontext_t ScrCtx;
#endif
struct ScrFiber {
+#ifdef SCR_WORKERS
+ struct ScrFiber *context_next;
+ struct ScrFiber **context_previous;
+ ScrPromise *waiting;
+#endif
ScrCtx ctx;
/* wasm32 uses LLVM's stackless switched-coroutine frame instead of a
* native saved stack. The compiler emits these two generic wrappers in
@@ -432,8 +437,15 @@ struct ScrFiber {
* scr_async_dyn.c (gated — the size-class stance); this always-linked
* core keeps only what the fiber machinery itself touches: the active
* slot, the snapshot RC pair, and the switch/spawn/destroy wiring. */
-static ScrAlsCtx *scr_als_main_slot = NULL;
+static SCR_TL ScrAlsCtx *scr_als_main_slot = NULL;
+#ifdef SCR_WORKERS
+SCR_TL ScrAlsCtx **scr_als_active;
+ScrAlsCtx **scr_als_slot(void) {
+ return scr_als_active ? scr_als_active : &scr_als_main_slot;
+}
+#else
ScrAlsCtx **scr_als_active = &scr_als_main_slot;
+#endif
ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c) {
if (c) c->rc++;
@@ -448,10 +460,22 @@ void scr_als_ctx_release(ScrAlsCtx *c) {
-static ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */
-static ScrFiber *scr_current = NULL;
-static long scr_fibers_live = 0;
-static long scr_fibers_abandoned = 0;
+static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */
+static SCR_TL ScrFiber *scr_current = NULL;
+static SCR_TL long scr_fibers_live = 0;
+static SCR_TL long scr_fibers_abandoned = 0;
+
+#ifdef SCR_WORKERS
+static SCR_TL ScrFiber *scr_context_fibers;
+static void scr_fiber_track(ScrFiber *fiber) {
+ fiber->context_next = scr_context_fibers;
+ fiber->context_previous = &scr_context_fibers;
+ if (scr_context_fibers) scr_context_fibers->context_previous = &fiber->context_next;
+ scr_context_fibers = fiber;
+}
+#else
+static void scr_fiber_track(ScrFiber *fiber) { (void)fiber; }
+#endif
long scr_abandoned_fiber_count(void) { return scr_fibers_abandoned; }
@@ -475,8 +499,8 @@ void scr_wasi_coro_started(void *handle) {
#endif
/* Microtask queue: ready fibers, FIFO. */
-static ScrFiber **scr_ready = NULL;
-static size_t scr_ready_head = 0, scr_ready_len = 0, scr_ready_cap = 0;
+static SCR_TL ScrFiber **scr_ready = NULL;
+static SCR_TL size_t scr_ready_head = 0, scr_ready_len = 0, scr_ready_cap = 0;
static void scr_ready_push(ScrFiber *f) {
if (scr_ready_head + scr_ready_len == scr_ready_cap) {
@@ -511,14 +535,14 @@ typedef struct {
double delay_ms; /* the original delay — refresh() re-arms to now + this */
} ScrTimer;
-static ScrTimer *scr_timers = NULL;
-static size_t scr_ntimers = 0, scr_timers_cap = 0;
-static unsigned long scr_timer_seq = 0;
+static SCR_TL ScrTimer *scr_timers = NULL;
+static SCR_TL size_t scr_ntimers = 0, scr_timers_cap = 0;
+static SCR_TL unsigned long scr_timer_seq = 0;
/* REF'd armed timers — the loop's timer-liveness count (an unref'd timer
* sits in the heap and still FIRES if the loop runs for other reasons, but
* does not by itself keep the loop alive). Kept in sync by push/pop/remove
* and by ref/unref. */
-static size_t scr_reffed_timers = 0;
+static SCR_TL size_t scr_reffed_timers = 0;
/* Exported: the island's timer machinery (scr_web.c) shares this clock so
* its deadlines are comparable with the loop's. */
@@ -600,11 +624,11 @@ void scr_set_timeout(ScrClosure *cb, double ms) {
* its callback runs, so the run loop re-pushes it only if the callback
* did not clear it. */
-static unsigned long scr_interval_next_id = 1;
-static unsigned long scr_firing_id = 0; /* interval currently running */
-static bool scr_firing_cleared = false;
-static bool scr_firing_reffed = true; /* the running interval's ref state */
-static bool scr_firing_refresh = false; /* refresh() called mid-callback */
+static SCR_TL unsigned long scr_interval_next_id = 1;
+static SCR_TL unsigned long scr_firing_id = 0; /* interval currently running */
+static SCR_TL bool scr_firing_cleared = false;
+static SCR_TL bool scr_firing_reffed = true; /* the running interval's ref state */
+static SCR_TL bool scr_firing_refresh = false; /* refresh() called mid-callback */
/* Removes heap entry i (swap-with-last, then sift whichever way the moved
* entry needs). */
@@ -740,7 +764,7 @@ bool scr_timer_has_ref(double handle) {
* callback). Resource kinds this runtime does not model as loop handles
* (TCP wraps, FS requests, ...) are absent — SEMANTICS.md names the
* divergence. Result +1. */
-static size_t scr_pending_immediates; /* defined below with the queue */
+static SCR_TL size_t scr_pending_immediates; /* defined below with the queue */
ScrArr *scr_active_resources(void) {
ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 8);
size_t timeouts = scr_ntimers + ((scr_firing_id != 0 && !scr_firing_cleared) ? 1 : 0);
@@ -763,8 +787,8 @@ typedef struct ScrNtick {
struct ScrNtick *next;
} ScrNtick;
-static ScrNtick *scr_nt_head = NULL;
-static ScrNtick *scr_nt_tail = NULL;
+static SCR_TL ScrNtick *scr_nt_head = NULL;
+static SCR_TL ScrNtick *scr_nt_tail = NULL;
/* Public: releases every queued tick without running it — the loop-exit
* teardown below AND the exit-listener runner (scr_events.c) both call
@@ -860,13 +884,13 @@ typedef struct {
bool reffed; /* keeps the loop alive; unref() clears it (Node semantics) */
} ScrImmediate;
-static ScrImmediate *scr_immediates = NULL;
-static size_t scr_nimmediates = 0, scr_immediates_head = 0, scr_immediates_cap = 0;
-static unsigned long scr_immediate_seq = 0;
+static SCR_TL ScrImmediate *scr_immediates = NULL;
+static SCR_TL size_t scr_nimmediates = 0, scr_immediates_head = 0, scr_immediates_cap = 0;
+static SCR_TL unsigned long scr_immediate_seq = 0;
/* Pending (queued, uncleared) immediates and the reffed subset — the
* loop's no-sleep signal and liveness count respectively. */
-static size_t scr_pending_immediates = 0;
-static size_t scr_reffed_immediates = 0;
+static SCR_TL size_t scr_pending_immediates = 0;
+static SCR_TL size_t scr_reffed_immediates = 0;
double scr_set_immediate(ScrClosure *cb) {
if (scr_nimmediates == scr_immediates_cap) {
@@ -945,8 +969,8 @@ static void scr_immediates_teardown(void) {
}
/* Unhandled rejections: rejected promises retained here until observed. */
-static ScrPromise **scr_maybe_unhandled = NULL;
-static size_t scr_nunhandled = 0, scr_unhandled_cap = 0;
+static SCR_TL ScrPromise **scr_maybe_unhandled = NULL;
+static SCR_TL size_t scr_nunhandled = 0, scr_unhandled_cap = 0;
static void scr_track_rejection(ScrPromise *p) {
if (scr_nunhandled == scr_unhandled_cap) {
@@ -960,7 +984,7 @@ static void scr_track_rejection(ScrPromise *p) {
/* ── context switching (ASan-annotated) ───────────────────────────────── */
#ifdef SCR_ASAN_FIBERS
-static void *scr_main_fake_stack; /* fake-stack slot for the main context */
+static SCR_TL void *scr_main_fake_stack; /* fake-stack slot for the main context */
#endif
#ifdef _WIN32
@@ -1082,7 +1106,12 @@ static void scr_promise_all_settle(ScrAllState *st, ScrPromise *result, size_t i
* destination's own pending check. Promise.all entries dispatch through
* their shared countdown state instead of the adapt pair. */
static void scr_promise_settle_wake(ScrPromise *p) {
- for (size_t i = 0; i < p->nwaiters; i++) scr_ready_push(p->waiters[i]);
+ for (size_t i = 0; i < p->nwaiters; i++) {
+#ifdef SCR_WORKERS
+ p->waiters[i]->waiting = NULL;
+#endif
+ scr_ready_push(p->waiters[i]);
+ }
p->nwaiters = 0;
for (size_t i = 0; i < p->ncbs; i++) {
if (p->cbs[i].all) {
@@ -1254,6 +1283,15 @@ static void scr_fiber_finish(ScrFiber *self) {
return;
}
ScrPromise *p = self->promise;
+#ifdef SCR_WORKERS
+ if (scr_context_stopping()) {
+ scr_context_checkpoint();
+ scr_promise_reject_from_cell(p, &self->exc);
+ p->rejection_observed = true;
+ self->done = true;
+ return;
+ }
+#endif
if (scr_exc_pending()) {
/* The body's exception escaped: the promise rejects with the payload
* (moved from the fiber's cell into the promise). */
@@ -1318,6 +1356,12 @@ static void scr_fiber_context_init(ScrFiber *f) {
* fiber object and its stack — legal here because a finished fiber has
* switched away and can never be current again. */
static void scr_fiber_destroy(ScrFiber *f) {
+#ifdef SCR_WORKERS
+ if (f->context_previous) {
+ *f->context_previous = f->context_next;
+ if (f->context_next) f->context_next->context_previous = f->context_previous;
+ }
+#endif
#ifdef _WIN32
DeleteFiber(f->ctx);
#elif defined(__wasi__)
@@ -1337,12 +1381,13 @@ static void scr_fiber_destroy(ScrFiber *f) {
ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) {
ScrFiber *f = calloc(1, sizeof *f);
if (!f) scr_oom();
+ scr_fiber_track(f);
f->promise = scr_promise_new();
f->entry = entry;
f->argpack = argpack;
/* AsyncLocalStorage: the child runs in the SPAWNER's context (Node's
* init-time capture); snapshots are immutable, so a retain suffices. */
- f->als = scr_als_ctx_retain(*scr_als_active);
+ f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
scr_fibers_live++;
#ifdef _WIN32
@@ -1413,7 +1458,7 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency,
f->promise = scr_promise_new();
f->entry = entry;
f->argpack = argpack;
- f->als = scr_als_ctx_retain(*scr_als_active);
+ f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
scr_fibers_live++;
if (dependency->state == SCR_PROM_PENDING) {
@@ -1444,7 +1489,22 @@ static void scr_await_park(ScrPromise *p) {
if (!p->waiters) scr_oom();
}
p->waiters[p->nwaiters++] = self;
+#ifdef SCR_WORKERS
+ self->waiting = p;
+#endif
scr_switch(&self->ctx, self->return_to, NULL);
+#ifdef SCR_WORKERS
+ /* Shutdown resumes suspended stacks without settling their dependencies.
+ * Remove the borrowed waiter before the stack releases its promise. */
+ if (self->waiting) {
+ for (size_t i = 0; i < p->nwaiters; i++) {
+ if (p->waiters[i] != self) continue;
+ memmove(p->waiters + i, p->waiters + i + 1, (--p->nwaiters - i) * sizeof(*p->waiters));
+ break;
+ }
+ self->waiting = NULL;
+ }
+#endif
/* Resumed by the loop: return_to must now point at the loop's context. */
}
@@ -1517,6 +1577,7 @@ static void scr_promise_rethrow(ScrPromise *p) {
case SCR_EXC_PRIMITIVE_REF: scr_throw_primitive_ref(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
case SCR_EXC_OBJ: scr_throw_obj(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
case SCR_EXC_NONE:
+ case SCR_EXC_TERMINATE:
case SCR_EXC_GENRET: /* unreachable: the sentinel never settles a promise */
scr_throw_str(scr_str_new("undefined", 9));
break;
@@ -1525,6 +1586,9 @@ static void scr_promise_rethrow(ScrPromise *p) {
/* Await result extraction. Rejection re-throws into the awaiter. */
static bool scr_await_settled(ScrPromise *p) {
+#ifdef SCR_WORKERS
+ if (scr_context_checkpoint()) return false;
+#endif
#ifdef __wasi__
if (p->state == SCR_PROM_PENDING) {
fputs("scriptc: internal error: resumed before awaited promise settled\n", stderr);
@@ -1532,7 +1596,15 @@ static bool scr_await_settled(ScrPromise *p) {
}
#else
if (p->state != SCR_PROM_PENDING) scr_await_yield();
- while (p->state == SCR_PROM_PENDING) scr_await_park(p);
+ while (p->state == SCR_PROM_PENDING) {
+#ifdef SCR_WORKERS
+ if (scr_context_checkpoint()) return false;
+#endif
+ scr_await_park(p);
+ }
+#ifdef SCR_WORKERS
+ if (scr_context_checkpoint()) return false;
+#endif
#endif
scr_prom_observe(p);
if (p->state == SCR_PROM_REJECTED) {
@@ -1843,6 +1915,9 @@ ScrPromise *scr_fsp_rename(ScrStr *oldpath, ScrStr *newpath) {
* platform mutex publishes each result before the loop removes it from the
* completion queue. */
#if !defined(SCR_LIB) && !defined(__wasi__)
+#ifdef SCR_WORKERS
+typedef struct ScrFsRenameOwner ScrFsRenameOwner;
+#endif
typedef struct ScrFsRenameOp {
bool generic;
bool ran;
@@ -1856,13 +1931,28 @@ typedef struct ScrFsRenameOp {
ScrFsRenameFn fn;
int error;
struct ScrFsRenameOp *next;
+#ifdef SCR_WORKERS
+ ScrFsRenameOwner *owner;
+#endif
} ScrFsRenameOp;
static ScrFsRenameOp *scr_fs_rename_work = NULL;
static ScrFsRenameOp **scr_fs_rename_work_tail = &scr_fs_rename_work;
+#ifdef SCR_WORKERS
+struct ScrFsRenameOwner {
+ ScrFsRenameOp *done;
+ ScrFsRenameOp **tail;
+ size_t pending;
+};
+static SCR_TL ScrFsRenameOwner *scr_fs_rename_owner;
+#define scr_fs_rename_done (scr_fs_rename_owner->done)
+#define scr_fs_rename_done_tail (scr_fs_rename_owner->tail)
+#define scr_fs_rename_pending_count (scr_fs_rename_owner->pending)
+#else
static ScrFsRenameOp *scr_fs_rename_done = NULL;
static ScrFsRenameOp **scr_fs_rename_done_tail = &scr_fs_rename_done;
static size_t scr_fs_rename_pending_count = 0;
+#endif
static size_t scr_fs_rename_worker_count = 0;
static bool scr_fs_rename_stopping = false;
static bool scr_fs_rename_shutdown_registered = false;
@@ -1903,6 +1993,41 @@ static void scr_fs_rename_op_release(ScrFsRenameOp *op) {
free(op);
}
+#ifdef SCR_WORKERS
+static void scr_fs_rename_context_cleanup(void) {
+ ScrFsRenameOwner *owner = scr_fs_rename_owner;
+ /* Outstanding native work may still borrow payload storage. Wait for its
+ * publication before reclaiming it, without invoking script callbacks
+ * during context teardown. Other script contexts keep using the pool. */
+ while (owner->pending) {
+ scr_fs_rename_lock_enter();
+ while (!owner->done) scr_fs_rename_wait();
+ ScrFsRenameOp *op = owner->done;
+ owner->done = op->next;
+ if (!owner->done) owner->tail = &owner->done;
+ scr_fs_rename_lock_leave();
+ owner->pending--;
+ scr_fs_rename_op_release(op);
+ }
+ scr_fs_rename_owner = NULL;
+ free(owner);
+}
+
+static ScrFsRenameOwner *scr_fs_rename_context(void) {
+ if (!scr_fs_rename_owner) {
+ ScrFsRenameOwner *owner = calloc(1, sizeof(*owner));
+ if (!owner) scr_oom();
+ owner->tail = &owner->done;
+ if (scr_atexit(scr_fs_rename_context_cleanup) != 0) {
+ free(owner);
+ scr_trap("scriptc: could not register work context cleanup\n");
+ }
+ scr_fs_rename_owner = owner;
+ }
+ return scr_fs_rename_owner;
+}
+#endif
+
static void scr_fs_rename_worker_loop(void) {
for (;;) {
scr_fs_rename_lock_enter();
@@ -1922,8 +2047,14 @@ static void scr_fs_rename_worker_loop(void) {
scr_fs_rename_lock_enter();
op->next = NULL;
+#ifdef SCR_WORKERS
+ *op->owner->tail = op;
+ op->owner->tail = &op->next;
+ scr_fs_rename_broadcast();
+#else
*scr_fs_rename_done_tail = op;
scr_fs_rename_done_tail = &op->next;
+#endif
scr_fs_rename_lock_leave();
}
}
@@ -1963,6 +2094,15 @@ static void scr_fs_renames_shutdown(void) {
}
scr_fs_rename_lock_enter();
+#ifdef SCR_WORKERS
+ /* Every context drains its in-flight work before it exits. This final
+ * process hook owns only the pool threads and cannot touch runtime heaps. */
+ if (scr_fs_rename_work) {
+ scr_fs_rename_lock_leave();
+ scr_trap("scriptc: work queue survived context teardown\n");
+ }
+ scr_fs_rename_lock_leave();
+#else
ScrFsRenameOp *work = scr_fs_rename_work;
ScrFsRenameOp *done = scr_fs_rename_done;
scr_fs_rename_work = NULL;
@@ -1981,11 +2121,20 @@ static void scr_fs_renames_shutdown(void) {
scr_fs_rename_op_release(done);
done = next;
}
+#endif
}
-static bool scr_fs_renames_pending(void) { return scr_fs_rename_pending_count != 0; }
+static bool scr_fs_renames_pending(void) {
+#ifdef SCR_WORKERS
+ if (!scr_fs_rename_owner) return false;
+#endif
+ return scr_fs_rename_pending_count != 0;
+}
static bool scr_fs_renames_dispatch(void) {
+#ifdef SCR_WORKERS
+ if (!scr_fs_rename_owner) return false;
+#endif
scr_fs_rename_lock_enter();
ScrFsRenameOp *op = scr_fs_rename_done;
if (op != NULL) {
@@ -2017,14 +2166,21 @@ static bool scr_fs_renames_dispatch(void) {
}
static void scr_work_submit_op(ScrFsRenameOp *op) {
+#ifdef SCR_WORKERS
+ op->owner = scr_fs_rename_context();
+ scr_fs_rename_pending_count++;
+ scr_fs_rename_lock_enter();
+#endif
if (!scr_fs_rename_shutdown_registered) {
if (atexit(scr_fs_renames_shutdown) != 0) {
scr_trap("scriptc: could not register worker cleanup\n");
}
scr_fs_rename_shutdown_registered = true;
}
+#ifndef SCR_WORKERS
scr_fs_rename_pending_count++;
scr_fs_rename_lock_enter();
+#endif
*scr_fs_rename_work_tail = op;
scr_fs_rename_work_tail = &op->next;
int create_error = 0;
@@ -2055,8 +2211,14 @@ static void scr_work_submit_op(ScrFsRenameOp *op) {
failed->error = create_error;
failed->ran = !failed->generic;
failed->next = NULL;
+#ifdef SCR_WORKERS
+ *failed->owner->tail = failed;
+ failed->owner->tail = &failed->next;
+ scr_fs_rename_broadcast();
+#else
*scr_fs_rename_done_tail = failed;
scr_fs_rename_done_tail = &failed->next;
+#endif
}
scr_fs_rename_work_tail = &scr_fs_rename_work;
} else {
@@ -2198,10 +2360,14 @@ typedef struct ScrCryptoBytesOp {
struct ScrCryptoBytesOp *next;
} ScrCryptoBytesOp;
-static ScrCryptoBytesOp *scr_crypto_bytes_head = NULL;
-static ScrCryptoBytesOp **scr_crypto_bytes_tail = &scr_crypto_bytes_head;
-static size_t scr_crypto_bytes_pending_count = 0;
-static bool scr_crypto_bytes_cleanup_registered = false;
+static SCR_TL ScrCryptoBytesOp *scr_crypto_bytes_head = NULL;
+#ifdef SCR_WORKERS
+static SCR_TL ScrCryptoBytesOp **scr_crypto_bytes_tail;
+#else
+static SCR_TL ScrCryptoBytesOp **scr_crypto_bytes_tail = &scr_crypto_bytes_head;
+#endif
+static SCR_TL size_t scr_crypto_bytes_pending_count = 0;
+static SCR_TL bool scr_crypto_bytes_cleanup_registered = false;
static void scr_crypto_bytes_shutdown(void) {
while (scr_crypto_bytes_head != NULL) {
@@ -2235,13 +2401,14 @@ static bool scr_crypto_bytes_dispatch(void) {
void scr_crypto_defer_bytes(ScrBytes *value, ScrError *error, ScrClosure *cb, ScrCryptoBytesFn fn) {
if (!scr_crypto_bytes_cleanup_registered) {
- if (atexit(scr_crypto_bytes_shutdown) != 0) {
+ if (scr_atexit(scr_crypto_bytes_shutdown) != 0) {
scr_bytes_release(value);
scr_error_release(error);
scr_closure_release(cb);
scr_trap("scriptc: could not register crypto callback cleanup\n");
}
scr_crypto_bytes_cleanup_registered = true;
+ scr_crypto_bytes_tail = &scr_crypto_bytes_head;
}
ScrCryptoBytesOp *op = malloc(sizeof *op);
if (!op) {
@@ -2371,10 +2538,10 @@ void scr_queue_microtask_dyn(const ScrDyn *cb) {
* code hint in scr_exception.c, so each unit stays self-contained (the
* runtime C tests link them separately). */
-static bool (*scr_events_pending_fn)(void) = NULL; /* keeps the loop alive */
-static bool (*scr_events_watching_fn)(void) = NULL; /* wants the poll sleep */
-static void (*scr_events_dispatch_fn)(void) = NULL; /* fire due listeners */
-static int (*scr_events_pollfds_fn)(int out[2]) = NULL;
+static SCR_TL bool (*scr_events_pending_fn)(void) = NULL; /* keeps the loop alive */
+static SCR_TL bool (*scr_events_watching_fn)(void) = NULL; /* wants the poll sleep */
+static SCR_TL void (*scr_events_dispatch_fn)(void) = NULL; /* fire due listeners */
+static SCR_TL int (*scr_events_pollfds_fn)(int out[2]) = NULL;
void scr_loop_set_events(bool (*pending)(void), bool (*watching)(void),
void (*dispatch)(void), int (*pollfds)(int out[2])) {
@@ -2467,8 +2634,8 @@ static void scr_resume_fiber(ScrFiber *f) {
* engine-job draining + fetch transfer polling here. Static builds never
* set it — both slots stay NULL and the loop is byte-identical in
* behavior. */
-static bool (*scr_io_pending_fn)(void) = NULL;
-static void (*scr_io_poll_fn)(double max_wait_ms) = NULL;
+static SCR_TL bool (*scr_io_pending_fn)(void) = NULL;
+static SCR_TL void (*scr_io_poll_fn)(double max_wait_ms) = NULL;
void scr_loop_set_io(bool (*pending)(void), void (*poll)(double)) {
scr_io_pending_fn = pending;
@@ -2479,7 +2646,7 @@ void scr_loop_set_io(bool (*pending)(void), void (*poll)(double)) {
* the sleep below so an armed AbortSignal.timeout fires on time while
* the loop waits on socket readiness — without keeping the loop alive by
* itself (the liveness test never consults it, Node's unref'd timer). */
-static double (*scr_island_deadline_fn)(void) = NULL;
+static SCR_TL double (*scr_island_deadline_fn)(void) = NULL;
void scr_loop_set_island_deadline(double (*fn)(void)) { scr_island_deadline_fn = fn; }
@@ -2489,9 +2656,9 @@ void scr_loop_set_island_deadline(double (*fn)(void)) { scr_island_deadline_fn =
* fd for the idle poll(2) sleep (readable while events are pending —
* kqueue and epoll fds both behave this way).
* Net-free builds keep every slot NULL and the loop is byte-identical. */
-static bool (*scr_net_pending_fn)(void) = NULL;
-static void (*scr_net_dispatch_fn)(void) = NULL;
-static int (*scr_net_pollfd_fn)(void) = NULL;
+static SCR_TL bool (*scr_net_pending_fn)(void) = NULL;
+static SCR_TL void (*scr_net_dispatch_fn)(void) = NULL;
+static SCR_TL int (*scr_net_pollfd_fn)(void) = NULL;
void scr_loop_set_net(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)) {
scr_net_pending_fn = pending;
@@ -2502,9 +2669,9 @@ void scr_loop_set_net(bool (*pending)(void), void (*dispatch)(void), int (*pollf
/* The dgram hook (scr_dgram.c, when linked) — the net hook's exact shape:
* one more set of nullable slots, byte-identical loop behavior when
* unset. */
-static bool (*scr_dgram_pending_fn)(void) = NULL;
-static void (*scr_dgram_dispatch_fn)(void) = NULL;
-static int (*scr_dgram_pollfd_fn)(void) = NULL;
+static SCR_TL bool (*scr_dgram_pending_fn)(void) = NULL;
+static SCR_TL void (*scr_dgram_dispatch_fn)(void) = NULL;
+static SCR_TL int (*scr_dgram_pollfd_fn)(void) = NULL;
void scr_loop_set_dgram(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)) {
scr_dgram_pending_fn = pending;
@@ -2515,9 +2682,9 @@ void scr_loop_set_dgram(bool (*pending)(void), void (*dispatch)(void), int (*pol
/* The fs.watch hook (scr_watch.c, when linked) — the net hook's exact
* shape: one more set of nullable slots, byte-identical loop behavior
* when unset. */
-static bool (*scr_watch_pending_fn)(void) = NULL;
-static void (*scr_watch_dispatch_fn)(void) = NULL;
-static int (*scr_watch_pollfd_fn)(void) = NULL;
+static SCR_TL bool (*scr_watch_pending_fn)(void) = NULL;
+static SCR_TL void (*scr_watch_dispatch_fn)(void) = NULL;
+static SCR_TL int (*scr_watch_pollfd_fn)(void) = NULL;
void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)) {
scr_watch_pending_fn = pending;
@@ -2528,10 +2695,10 @@ void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pol
/* The foreign-FFI queue hook (scr_ffi.c when a format-5 descriptor exists).
* Its pending count includes live registrations (reffed by default) and
* queued deliveries; dispatch runs exactly one callback per loop turn. */
-static bool (*scr_ffi_pending_fn)(void) = NULL;
-static bool (*scr_ffi_dispatch_fn)(void) = NULL;
-static int (*scr_ffi_pollfd_fn)(void) = NULL;
-static void (*scr_ffi_stop_fn)(void) = NULL;
+static SCR_TL bool (*scr_ffi_pending_fn)(void) = NULL;
+static SCR_TL bool (*scr_ffi_dispatch_fn)(void) = NULL;
+static SCR_TL int (*scr_ffi_pollfd_fn)(void) = NULL;
+static SCR_TL void (*scr_ffi_stop_fn)(void) = NULL;
void scr_loop_set_ffi(bool (*pending)(void), bool (*dispatch)(void),
int (*pollfd)(void), void (*stop)(void)) {
@@ -2546,14 +2713,53 @@ void scr_loop_set_ffi(bool (*pending)(void), bool (*dispatch)(void),
* TOP of every turn — before the events/net stations, the closest
* placement to Node's process.nextTick (whose stream emissions these
* are). No poller fd: ticks are pure CPU work, always ready. */
-static bool (*scr_stream_pending_fn)(void) = NULL;
-static void (*scr_stream_dispatch_fn)(void) = NULL;
+static SCR_TL bool (*scr_stream_pending_fn)(void) = NULL;
+static SCR_TL void (*scr_stream_dispatch_fn)(void) = NULL;
void scr_loop_set_stream(bool (*pending)(void), void (*dispatch)(void)) {
scr_stream_pending_fn = pending;
scr_stream_dispatch_fn = dispatch;
}
+#ifdef SCR_WORKERS
+static SCR_TL bool (*scr_workers_pending_fn)(void);
+static SCR_TL void (*scr_workers_dispatch_fn)(void);
+static SCR_TL int (*scr_workers_pollfd_fn)(void);
+static SCR_TL bool (*scr_workers_ready_fn)(void);
+static SCR_TL void (*scr_workers_wait_fn)(double);
+
+void scr_loop_set_workers(bool (*pending)(void), bool (*ready)(void), void (*dispatch)(void),
+ int (*pollfd)(void), void (*wait)(double)) {
+ scr_workers_pending_fn = pending;
+ scr_workers_dispatch_fn = dispatch;
+ scr_workers_pollfd_fn = pollfd;
+ scr_workers_ready_fn = ready;
+ scr_workers_wait_fn = wait;
+}
+#endif
+
+static bool scr_workers_pending(void) {
+#ifdef SCR_WORKERS
+ return scr_workers_pending_fn && scr_workers_pending_fn();
+#else
+ return false;
+#endif
+}
+
+static void scr_workers_dispatch(void) {
+#ifdef SCR_WORKERS
+ if (scr_workers_dispatch_fn) scr_workers_dispatch_fn();
+#endif
+}
+
+static int scr_workers_pollfd(void) {
+#ifdef SCR_WORKERS
+ return scr_workers_pollfd_fn ? scr_workers_pollfd_fn() : -1;
+#else
+ return -1;
+#endif
+}
+
/* Dispatch hooks yield between event batches when fibers are already
* queued, so promise jobs interleave with event delivery (Node's
* microtask checkpoints between macrotasks). */
@@ -2659,6 +2865,9 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
if (scr_ready_len > 0) continue;
if (scr_stream_pending_fn != NULL && scr_stream_pending_fn()) continue;
}
+ scr_workers_dispatch();
+ if (scr_exc_pending()) return false;
+ if (scr_ready_len > 0) continue;
/* Event dispatch (scr_events.c, when linked): watched signals
* delivered since the last turn fire their listeners now (macrotasks,
* like timers), then stdin — while a consumer exists, probe fd 0 and
@@ -2717,7 +2926,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
(scr_dgram_pending_fn != NULL && scr_dgram_pending_fn()) ||
(scr_watch_pending_fn != NULL && scr_watch_pending_fn()) ||
(scr_ffi_pending_fn != NULL && scr_ffi_pending_fn()) ||
- scr_fs_renames_pending() || scr_crypto_bytes_pending();
+ scr_fs_renames_pending() || scr_crypto_bytes_pending() || scr_workers_pending();
if (held) {
scr_children_poll();
if (scr_exc_pending()) return false; /* uncaught throw in a listener */
@@ -2739,13 +2948,14 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
bool ffi = scr_ffi_pending_fn != NULL && scr_ffi_pending_fn();
bool renames = scr_fs_renames_pending();
bool crypto = scr_crypto_bytes_pending();
+ bool workers = scr_workers_pending();
/* Timer liveness counts only REF'd timers: an unref'd timer stays in
* the heap (and fires if the loop runs on for other reasons) but does
* not by itself keep the process alive — Node's unref semantics.
* Children follow the same rule: an unref'd child is still REAPED
* while the loop runs (kids drives the sweeps and sleeps above) but
* only reffed ones keep the process alive. */
- if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !scr_children_reffed_pending() && !io && !events && !net && !dgram && !watch && !ffi && !renames && !crypto) break;
+ if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !scr_children_reffed_pending() && !io && !events && !net && !dgram && !watch && !ffi && !renames && !crypto && !workers) break;
/* Sleep to the earliest deadline, then run every due timer (each may
* enqueue microtasks, which the next iteration drains first). Who
* sleeps depends on what is pending:
@@ -2784,21 +2994,30 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
* during that turn. No fd will wake us for this userspace work: return to
* dispatch without sleeping, still allowing due timers and immediates. */
if (scr_children_ready()) due = now;
+#ifdef SCR_WORKERS
+ if (scr_workers_ready_fn && scr_workers_ready_fn()) due = now;
+#endif
#if !defined(_WIN32) && !defined(__wasi__)
if (due > now) scr_fiber_stacks_clear();
#endif
bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn();
+#ifdef SCR_WORKERS
+ if (workers && scr_workers_wait_fn && !io && !kids && !evw && !net && !dgram && !watch && !ffi && !renames && !crypto) {
+ scr_workers_wait_fn(due > now ? due - now : 0);
+ now = scr_now_ms();
+ } else
+#endif
if (io) {
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
/* Signals/stdin/net can't wake curl's fd wait (and its poll retries
* on EINTR), so they re-impose a coarser cap — bounded Ctrl-C and
* socket latency during a fetch, without the reap-granularity
* cost. */
- else if ((evw || net || dgram || watch || ffi) && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
+ else if ((evw || net || dgram || watch || ffi || workers) && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
scr_io_poll_fn(due > now ? due - now : 0);
now = scr_now_ms();
if (scr_ready_len > 0) continue; /* io callbacks woke fibers */
- } else if (evw || net || dgram || watch || ffi) {
+ } else if (evw || net || dgram || watch || ffi || workers) {
#if defined(_WIN32) || defined(__wasi__)
/* The win32 arm, and WASI hosts whose poll_oneoff adapters do not
* reliably wake for a closed inherited stdin pipe: the sleep is a capped nanosleep and
@@ -2813,7 +3032,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
* WaitForMultipleObjects over WSAEVENTs, or IOCP. */
if (evw && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
if ((net || dgram || watch) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
- if (ffi && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
+ if ((ffi || workers) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
if (due > now) {
double wait = due - now;
@@ -2830,7 +3049,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
* events are pending); unrepresentable children keep the ~1ms reap cap
* instead. Dispatch happens at the next turn's top — the poll only
* decides how long to sleep. */
- struct pollfd fds[7];
+ struct pollfd fds[8];
int nfds = 0;
int evfds[2];
int nev = evw && scr_events_pollfds_fn != NULL ? scr_events_pollfds_fn(evfds) : 0;
@@ -2895,6 +3114,16 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
due = now + SCR_CHILD_POLL_MS;
}
}
+ if (workers) {
+ int worker_fd = scr_workers_pollfd();
+ if (worker_fd >= 0) {
+ fds[nfds].fd = worker_fd;
+ fds[nfds].events = POLLIN;
+ fds[nfds++].revents = 0;
+ } else if (due > now + SCR_CHILD_POLL_MS) {
+ due = now + SCR_CHILD_POLL_MS;
+ }
+ }
double wait = due > now ? due - now : 0;
if (wait > 1e9) wait = 1e9; /* poll takes int ms */
(void)poll(fds, (nfds_t)nfds, (int)(wait + 0.999));
@@ -3047,8 +3276,8 @@ bool scr_loop_run(ScrPromise *top_level) {
* ledger below reported nothing — one report, one voice, like Node's
* first-unhandled-rejection death. Returns whether the island had any.
* Static builds never set it. */
-static bool (*scr_island_rejections_fn)(bool print) = NULL;
-static int (*scr_island_jobs_drain_fn)(void) = NULL;
+static SCR_TL bool (*scr_island_rejections_fn)(bool print) = NULL;
+static SCR_TL int (*scr_island_jobs_drain_fn)(void) = NULL;
void scr_loop_set_island_rejections(bool (*fn)(bool print),
int (*drain_jobs)(void)) {
@@ -3065,7 +3294,7 @@ void scr_loop_set_island_rejections(bool (*fn)(bool print),
* registration — the loop-hook pattern, so listener-free binaries keep
* their size class): called per never-observed rejection; false = a
* listener threw (the uncaught crash path). */
-bool (*scr_urj_deliver_fn)(ScrPromise *p) = NULL;
+SCR_TL bool (*scr_urj_deliver_fn)(ScrPromise *p) = NULL;
/* Unhandled rejections at a completed nextTick/microtask checkpoint:
* Node prints an error and exits 1 when no listener handles the event.
@@ -3108,6 +3337,14 @@ bool scr_report_unhandled_rejections(void) {
}
} else if (!any) {
any = true;
+#ifdef SCR_WORKERS
+ if (scr_context_report_error) {
+ scr_promise_rethrow(p);
+ scr_exc_print_uncaught();
+ scr_promise_release(p);
+ continue;
+ }
+#endif
fflush(stdout);
fputs("Unhandled promise rejection: ", stderr);
switch (p->payload_kind) {
@@ -3386,6 +3623,9 @@ struct ScrGen {
ScrExcCell async_error;
bool settle_queued;
bool success_hop_done;
+#ifdef SCR_WORKERS
+ bool context_abandoned;
+#endif
};
static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpack,
@@ -3404,12 +3644,13 @@ static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpa
ScrFiber *f = calloc(1, sizeof *f);
if (!f) scr_oom();
f->gen = g;
+ scr_fiber_track(f);
f->entry = entry;
f->argpack = argpack; /* owned by the fiber start; drop_args if never started */
/* AsyncLocalStorage: generator bodies run in their CREATOR's context
* (the spawn-inheritance stance; resumes swap it active like any
* fiber switch). */
- f->als = scr_als_ctx_retain(*scr_als_active);
+ f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
scr_fibers_live++;
#ifdef _WIN32
f->ctx = CreateFiberEx(4096, 0, 0, scr_trampoline, NULL);
@@ -3441,6 +3682,16 @@ ScrGen *scr_gen_retain(ScrGen *g) {
void scr_gen_release(ScrGen *g) {
if (!g || --g->rc != 0) return;
+#ifdef SCR_WORKERS
+ if (g->fiber && g->state != SCR_GEN_UNSTARTED && g->state != SCR_GEN_DONE) {
+ /* A dropped suspended generator cannot run user finalizers. Keep its
+ * native frame valid until context shutdown can unwind owned locals
+ * through the noncatchable stop path. */
+ g->rc = 1;
+ g->context_abandoned = true;
+ return;
+ }
+#endif
scr_dyn_release(g->receiver);
scr_gen_slot_reset(&g->out);
scr_gen_slot_reset(&g->in);
@@ -3995,3 +4246,82 @@ static void scr_async_gen_resume_ready(ScrGen *g) {
}
scr_gen_release(g);
}
+
+#ifdef SCR_WORKERS
+static void scr_context_ready_discard(void) {
+ while (scr_ready_len) {
+ ScrFiber *fiber = scr_ready[scr_ready_head++];
+ scr_ready_len--;
+ if (fiber->micro_cb) {
+ scr_closure_release(fiber->micro_cb);
+ free(fiber);
+ } else if (fiber->gen_job) {
+ ScrGen *generator = fiber->gen_job;
+ generator->settle_queued = false;
+ scr_gen_release(generator);
+ free(fiber);
+ }
+ /* Real fibers belong to the context registry, including queued ones. */
+ }
+ scr_ready_head = 0;
+}
+
+/* Thread exit reclaims suspended native stacks by resuming only their
+ * compiler-owned unwind paths. No JavaScript catch/finally or queued user
+ * callback runs after the stop flag is installed. */
+void scr_loop_context_shutdown(void) {
+ if (scr_current) scr_trap("scriptc: context shutdown on an active fiber\n");
+ scr_context_stop(scr_exit_code_hint_get());
+ scr_timers_teardown();
+ scr_context_ready_discard();
+ while (scr_context_fibers) {
+ ScrFiber *fiber = scr_context_fibers;
+ if (fiber->gen) {
+ ScrGen *generator = scr_gen_retain(fiber->gen);
+ if (generator->state == SCR_GEN_UNSTARTED) {
+ if (generator->drop_args) generator->drop_args(fiber->argpack);
+ else free(fiber->argpack);
+ scr_fiber_destroy(fiber);
+ generator->fiber = NULL;
+ generator->state = SCR_GEN_DONE;
+ scr_fibers_live--;
+ } else {
+ scr_gen_switch_in(generator);
+ if (generator->fiber) scr_trap("scriptc: generator did not unwind during shutdown\n");
+ }
+ while (generator->requests_head) {
+ ScrAsyncGenRequest *request = generator->requests_head;
+ generator->requests_head = request->next;
+ scr_gen_slot_reset(&request->arg);
+ scr_gen_exc_reset(&request->thrown);
+ scr_promise_release(request->promise);
+ free(request);
+ scr_gen_release(generator);
+ }
+ generator->requests_tail = NULL;
+ if (generator->active_request) {
+ scr_promise_release(generator->active_request);
+ generator->active_request = NULL;
+ scr_gen_release(generator);
+ }
+ if (generator->context_abandoned) {
+ generator->context_abandoned = false;
+ scr_gen_release(generator);
+ }
+ scr_gen_release(generator);
+ } else {
+ scr_resume_fiber(fiber);
+ if (scr_context_fibers == fiber) scr_trap("scriptc: async function did not unwind during shutdown\n");
+ }
+ scr_context_ready_discard();
+ }
+ scr_discard_unhandled_rejections();
+ free(scr_ready); scr_ready = NULL; scr_ready_cap = 0;
+ free(scr_timers); scr_timers = NULL; scr_timers_cap = 0;
+ free(scr_immediates); scr_immediates = NULL; scr_immediates_cap = 0;
+ free(scr_maybe_unhandled); scr_maybe_unhandled = NULL; scr_unhandled_cap = 0;
+ scr_als_ctx_release(scr_als_main_slot); scr_als_main_slot = NULL;
+ scr_fibers_abandoned = 0;
+ scr_note_abandoned_fibers(0);
+}
+#endif
diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c
index 5900f4d81..675c2ae3b 100644
--- a/packages/runtime/src/scr_async_dyn.c
+++ b/packages/runtime/src/scr_async_dyn.c
@@ -38,28 +38,28 @@ static ScrAlsCtx *scr_als_ctx_alloc(size_t len) {
return c;
}
-static double scr_als_counter = 0;
+static SCR_TL double scr_als_counter = 0;
/* Main-slot teardown (atexit, LIFO after scr_init's audit registration —
* the dc-registry precedent): a top-level enterWith leaves a context in
* the main slot at exit; release it before the RC audit counts. */
static void scr_als_teardown(void) {
/* atexit runs on the main context — the active slot IS main's. */
- scr_als_ctx_release(*scr_als_active);
- *scr_als_active = NULL;
+ scr_als_ctx_release(*SCR_ALS_SLOT());
+ *SCR_ALS_SLOT() = NULL;
}
double scr_als_new(void) {
- static bool teardown_registered = false;
+ static SCR_TL bool teardown_registered = false;
if (!teardown_registered) {
teardown_registered = true;
- atexit(scr_als_teardown);
+ scr_atexit(scr_als_teardown);
}
return ++scr_als_counter;
}
ScrDyn *scr_als_get(double id) {
- const ScrAlsCtx *c = *scr_als_active;
+ const ScrAlsCtx *c = *SCR_ALS_SLOT();
if (c) {
for (size_t i = 0; i < c->len; i++) {
if (c->entries[i].id == id) return scr_dyn_retain(c->entries[i].value);
@@ -72,7 +72,7 @@ ScrDyn *scr_als_get(double id) {
* retained in), installed as the active context; the PREVIOUS snapshot
* returns (ownership moves out) for scr_als_restore. */
ScrAlsCtx *scr_als_enter(double id, ScrDyn *value) {
- ScrAlsCtx *prev = *scr_als_active;
+ ScrAlsCtx *prev = *SCR_ALS_SLOT();
size_t n = prev ? prev->len : 0;
bool have = false;
for (size_t i = 0; i < n; i++) {
@@ -88,13 +88,13 @@ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value) {
}
next->entries[w].id = id;
next->entries[w].value = scr_dyn_retain(value);
- *scr_als_active = next;
+ *SCR_ALS_SLOT() = next;
return prev; /* ownership moves to the caller */
}
/* The exit() arm: the id REMOVED from the snapshot. */
ScrAlsCtx *scr_als_enter_absent(double id) {
- ScrAlsCtx *prev = *scr_als_active;
+ ScrAlsCtx *prev = *SCR_ALS_SLOT();
size_t n = prev ? prev->len : 0;
size_t keep = 0;
for (size_t i = 0; i < n; i++) {
@@ -108,13 +108,13 @@ ScrAlsCtx *scr_als_enter_absent(double id) {
next->entries[w].value = scr_dyn_retain(prev->entries[i].value);
w++;
}
- *scr_als_active = next;
+ *SCR_ALS_SLOT() = next;
return prev;
}
void scr_als_restore(ScrAlsCtx *prev) {
- scr_als_ctx_release(*scr_als_active);
- *scr_als_active = prev; /* ownership moves back in */
+ scr_als_ctx_release(*SCR_ALS_SLOT());
+ *SCR_ALS_SLOT() = prev; /* ownership moves back in */
}
void scr_als_enter_with(double id, ScrDyn *value) {
@@ -166,12 +166,12 @@ static void scr_rej_release(ScrRejListener *l) {
if (--l->state->refs == 0) free(l->state);
}
-static uint64_t scr_rej_id = 0;
+static SCR_TL uint64_t scr_rej_id = 0;
-static ScrRejListener *scr_urj_listeners = NULL;
-static size_t scr_nurj = 0, scr_urj_cap = 0;
-static ScrRejListener *scr_rjh_listeners = NULL;
-static size_t scr_nrjh = 0, scr_rjh_cap = 0;
+static SCR_TL ScrRejListener *scr_urj_listeners = NULL;
+static SCR_TL size_t scr_nurj = 0, scr_urj_cap = 0;
+static SCR_TL ScrRejListener *scr_rjh_listeners = NULL;
+static SCR_TL size_t scr_nrjh = 0, scr_rjh_cap = 0;
static void scr_urj_teardown(void) {
for (size_t i = 0; i < scr_nurj; i++) scr_rej_release(&scr_urj_listeners[i]);
@@ -239,10 +239,10 @@ static void scr_rjh_sync_hook(void) {
void scr_process_on_unhandled_rejection(ScrDyn *fn, bool once) {
if (!scr_rej_check_listener(fn)) return;
- static bool teardown_armed = false;
+ static SCR_TL bool teardown_armed = false;
if (!teardown_armed) {
teardown_armed = true;
- atexit(scr_urj_teardown);
+ scr_atexit(scr_urj_teardown);
}
scr_rej_push(&scr_urj_listeners, &scr_nurj, &scr_urj_cap, fn, once);
scr_urj_sync_hook();
@@ -255,10 +255,10 @@ void scr_process_off_unhandled_rejection(ScrDyn *fn) {
void scr_process_on_rejection_handled(ScrDyn *fn, bool once) {
if (!scr_rej_check_listener(fn)) return;
- static bool teardown_armed = false;
+ static SCR_TL bool teardown_armed = false;
if (!teardown_armed) {
teardown_armed = true;
- atexit(scr_rjh_teardown);
+ scr_atexit(scr_rjh_teardown);
}
scr_rej_push(&scr_rjh_listeners, &scr_nrjh, &scr_rjh_cap, fn, once);
scr_rjh_sync_hook();
@@ -307,8 +307,8 @@ static bool scr_rej_fire(ScrRejListener **list, size_t *n, ScrDyn **args, size_t
/* Uncaught exception listeners share the checked-dynamic callback ABI.
* Monitors run first but do not handle the exception by themselves. */
-static ScrRejListener *scr_uncaught_ls[2];
-static size_t scr_uncaught_n[2], scr_uncaught_cap[2];
+static SCR_TL ScrRejListener *scr_uncaught_ls[2];
+static SCR_TL size_t scr_uncaught_n[2], scr_uncaught_cap[2];
static int scr_uncaught_dispatch(bool from_promise);
static void scr_uncaught_sync_hook(void) {
@@ -327,8 +327,8 @@ static void scr_uncaught_teardown(void) {
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor) {
if (!scr_rej_check_listener(fn)) return;
- static bool armed = false;
- if (!armed) { armed = true; atexit(scr_uncaught_teardown); }
+ static SCR_TL bool armed = false;
+ if (!armed) { armed = true; scr_atexit(scr_uncaught_teardown); }
size_t k = monitor ? 1 : 0;
scr_rej_push(&scr_uncaught_ls[k], &scr_uncaught_n[k], &scr_uncaught_cap[k], fn, once);
scr_uncaught_sync_hook();
@@ -605,8 +605,8 @@ ScrDyn *scr_await_dyn_value_settled(ScrDyn *value) {
* an ScrError (identity-cached, so a listener comparing two deliveries
* of one warning sees one object); a string `detail` joins the dyn node
* and the report's second line, exactly Node. */
-static ScrDyn **scr_warn_listeners = NULL;
-static size_t scr_nwarn = 0, scr_warn_cap = 0;
+static SCR_TL ScrDyn **scr_warn_listeners = NULL;
+static SCR_TL size_t scr_nwarn = 0, scr_warn_cap = 0;
static void scr_warn_teardown(void) {
for (size_t i = 0; i < scr_nwarn; i++) scr_dyn_release(scr_warn_listeners[i]);
@@ -629,7 +629,7 @@ void scr_process_on_warning(ScrDyn *fn) {
abort();
}
}
- if (scr_nwarn == 0) atexit(scr_warn_teardown);
+ if (scr_nwarn == 0) scr_atexit(scr_warn_teardown);
scr_warn_listeners[scr_nwarn++] = scr_dyn_retain(fn);
}
@@ -671,7 +671,7 @@ static void scr_warning_dispatch(ScrDyn *w) {
if (ed && ed->kind == SCR_DYN_STR) fprintf(stderr, "\n%s", ed->v.str->data);
fputc('\n', stderr);
/* Node's one-time trace hint, after the first report. */
- static bool hinted = false;
+ static SCR_TL bool hinted = false;
if (!hinted) {
hinted = true;
fputs("(Use `node --trace-warnings ...` to show where the warning was created)\n", stderr);
@@ -850,7 +850,7 @@ ScrDyn *scr_promise_reason_dyn(const ScrPromise *p) {
* promise directly and every other inner type through the adapting
* constructor below. */
-extern bool (*scr_dyn_promise_identity_fn)(ScrPromise *, ScrPromise *);
+SCR_TL extern bool (*scr_dyn_promise_identity_fn)(ScrPromise *, ScrPromise *);
ScrDyn *scr_dyn_new_promise(ScrPromise *p) {
scr_dyn_promise_identity_fn = scr_promise_identity_equal;
diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c
index f05be6770..d73f515ec 100644
--- a/packages/runtime/src/scr_bytes.c
+++ b/packages/runtime/src/scr_bytes.c
@@ -132,6 +132,7 @@ ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len) {
b->is_buffer = false;
b->is_data_view = false;
b->external = false;
+ b->shared = NULL;
#ifdef SCR_RC_AUDIT
scr_live_bytes++;
#endif
@@ -199,6 +200,7 @@ ScrBytes *scr_bytes_from_data(const uint8_t *data, size_t len) {
}
ScrBytes *scr_bytes_copy(const ScrBytes *src) {
+ SCR_SHARED_GUARD(src, NULL);
ScrBytes *b = scr_bytes_alloc_private(src->elem, src->len);
memcpy(b->data, src->data, src->len * scr_bytes_elem_size(src->elem));
return b;
@@ -221,6 +223,8 @@ void scr_bytes_release(ScrBytes *b) {
scr_weak_dispose(b);
if (b->backing) {
scr_bytes_release(b->backing); /* a view: data points into the owner */
+ } else if (b->shared) {
+ scr_shared_release(b->shared);
} else if (!b->external) {
free(b->data);
}
@@ -237,6 +241,7 @@ void scr_bytes_release_v(void *b) { scr_bytes_release((ScrBytes *)b); }
double scr_bytes_len(const ScrBytes *b) { return (double)b->len; }
void scr_bytes_copy_contents(ScrBytes *dst, const ScrBytes *src) {
+ SCR_SHARED_GUARD(dst, src);
if (dst->elem != src->elem || dst->len != src->len) {
scr_trap("scriptc: typed array snapshot shape changed\n");
}
@@ -268,6 +273,7 @@ static uint32_t scr_bytes_to_u32(double v) {
}
double scr_bytes_get(const ScrBytes *b, double i) {
+ SCR_SHARED_GUARD(b, NULL);
size_t idx = scr_bytes_check_index(b, i);
switch (b->elem) {
case SCR_BYTES_U8: case SCR_BYTES_U8C:
@@ -312,6 +318,7 @@ double scr_bytes_get(const ScrBytes *b, double i) {
}
void scr_bytes_set(ScrBytes *b, double i, double v) {
+ SCR_SHARED_GUARD(b, NULL);
if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) return;
size_t idx = (size_t)i;
switch (b->elem) {
@@ -424,6 +431,7 @@ static void scr_bytes_convert_span(uint8_t *dst, ScrBytesElem dest_elem,
}
ScrBytes *scr_bytes_convert(ScrBytesElem elem, const ScrBytes *src) {
+ SCR_SHARED_GUARD(src, NULL);
if (elem == src->elem) return scr_bytes_copy(src);
ScrBytes *out = scr_bytes_alloc_private(elem, src->len);
scr_bytes_convert_span(out->data, elem, src->data, src->elem, src->len);
@@ -443,6 +451,7 @@ static size_t scr_bytes_rel_index(double i, size_t len) {
}
ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end) {
+ SCR_SHARED_GUARD(b, NULL);
size_t s = scr_bytes_rel_index(start, b->len);
size_t e = scr_bytes_rel_index(end, b->len);
size_t count = e > s ? e - s : 0;
@@ -453,6 +462,7 @@ ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end) {
}
ScrBytes *scr_bytes_copy_within(ScrBytes *b, double target, double start, double end) {
+ SCR_SHARED_GUARD(b, NULL);
size_t t = scr_bytes_rel_index(target, b->len);
size_t s = scr_bytes_rel_index(start, b->len);
size_t e = scr_bytes_rel_index(end, b->len);
@@ -468,6 +478,7 @@ ScrBytes *scr_bytes_copy_within(ScrBytes *b, double target, double start, double
* rounding), slice-clamped relative indices; answers the receiver +1
* (chaining). Never throws — Buffer's throwing fill family is separate. */
ScrBytes *scr_bytes_fill_elem(ScrBytes *b, double v, double start, double end) {
+ SCR_SHARED_GUARD(b, NULL);
size_t s = scr_bytes_rel_index(start, b->len);
size_t e = scr_bytes_rel_index(end, b->len);
if (e > s) {
@@ -511,6 +522,7 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) {
v->is_buffer = b->is_buffer;
v->is_data_view = false;
v->external = false;
+ v->shared = owner->shared;
#ifdef SCR_RC_AUDIT
scr_live_bytes++;
#endif
@@ -518,6 +530,7 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) {
}
void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset) {
+ SCR_SHARED_GUARD(dst, src);
double t = (offset != offset) ? 0 : trunc(offset);
if (!(t >= 0) || (double)src->len + t > (double)dst->len) {
static const char msg[] = "offset is out of bounds";
@@ -625,7 +638,7 @@ ScrBytes *scr_bytes_from_dyn(ScrBytesElem elem, const ScrDyn *value, bool from)
return out;
}
if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(elem, value->v.bytes);
- if (!from && scr_array_buffer_is(value)) return scr_array_buffer_view(elem, value, scr_dyn_undefined(), scr_dyn_undefined());
+ if (!from && scr_buffer_storage_is(value)) return scr_array_buffer_view(elem, value, scr_dyn_undefined(), scr_dyn_undefined());
if (!from && (value->kind == SCR_DYN_NUM || value->kind == SCR_DYN_BOOL ||
value->kind == SCR_DYN_STR || value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF)) {
double number;
@@ -736,6 +749,7 @@ ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double
v->is_buffer = false;
v->is_data_view = true;
v->external = false;
+ v->shared = owner->shared;
#ifdef SCR_RC_AUDIT
scr_live_bytes++;
#endif
@@ -784,6 +798,7 @@ static size_t scr_dataview_get_size(ScrDataViewGet kind) {
}
double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, bool le) {
+ SCR_SHARED_GUARD(b, NULL);
size_t width = scr_dataview_get_size(kind);
/* ToIndex, then the view-relative bounds check — Node's ONE constant
* message for every failure mode (negative, NaN is fine, too large). */
@@ -832,6 +847,7 @@ double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind,
}
void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet kind, bool le) {
+ SCR_SHARED_GUARD(b, NULL);
size_t width = scr_dataview_get_size(kind);
/* ToIndex + the view-relative bounds check — the getters' ONE message. */
double off = (byte_off != byte_off) ? 0 : trunc(byte_off);
@@ -982,6 +998,7 @@ ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len) {
* difference — a leading UTF-8 BOM is stripped (ignoreBOM defaults to
* false in the spec; Buffer.toString keeps the BOM as U+FEFF). */
ScrStr *scr_text_decode_options(const ScrBytes *b, bool fatal, bool ignore_bom) {
+ SCR_SHARED_GUARD(b, NULL);
const uint8_t *in = b->data;
size_t n = b->len;
if (!ignore_bom && n >= 3 && in[0] == 0xef && in[1] == 0xbb && in[2] == 0xbf) {
@@ -1004,7 +1021,7 @@ ScrBytes *scr_bytes_buffer_source(const ScrDyn *value) {
ScrBytes *bytes = value->v.bytes;
return scr_bytes_buffer_view(bytes, SCR_BYTES_U8, scr_bytes_byte_offset(bytes), true, scr_bytes_byte_len(bytes));
}
- if (scr_array_buffer_is(value)) {
+ if (scr_buffer_storage_is(value)) {
return scr_array_buffer_view(SCR_BYTES_U8, value, scr_dyn_undefined(), scr_dyn_undefined());
}
static const char message[] = "The input argument must be an instance of SharedArrayBuffer, ArrayBuffer or ArrayBufferView.";
@@ -1175,12 +1192,14 @@ static ScrStr *scr_strdec_step(const ScrStr *enc, double pending, const ScrBytes
/* decoder.write(chunk): the decoded complete prefix (+1). */
ScrStr *scr_strdec_write(const ScrStr *enc, double pending, const ScrBytes *chunk) {
+ SCR_SHARED_GUARD(chunk, NULL);
double hold;
return scr_strdec_step(enc, pending, chunk, true, &hold);
}
/* The decoder state AFTER a write: the packed trailing partial sequence. */
double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk) {
+ SCR_SHARED_GUARD(chunk, NULL);
double hold = 0;
scr_strdec_step(enc, pending, chunk, false, &hold);
return hold;
@@ -1221,6 +1240,7 @@ static void scr_bytes_to_str_bounds(const ScrBytes *b, double start, double end,
* form). Delegates to the whole-buffer decoder
* through a stack view — no allocation, no rc traffic. */
ScrStr *scr_bytes_to_str_range(const ScrBytes *b, const ScrStr *enc, double start, double end) {
+ SCR_SHARED_GUARD(b, NULL);
size_t s0, e0;
scr_bytes_to_str_bounds(b, start, end, &s0, &e0);
ScrBytes view = { .rc = 1, .len = e0 - s0, .elem = b->elem, .data = b->data + s0, .backing = NULL };
@@ -1823,6 +1843,7 @@ static ScrStr *scr_td_iso_2022_jp_decode(const ScrBytes *b, bool fatal) {
}
ScrStr *scr_text_decode_legacy_options(const ScrBytes *b, double encoding_value, bool fatal, bool ignore_bom) {
+ SCR_SHARED_GUARD(b, NULL);
unsigned encoding = (unsigned)encoding_value;
if (encoding == 36) encoding = 7; /* ISO-8859-8-I */
if (encoding == 37) encoding = SCR_TD_GB18030; /* GBK */
@@ -1863,6 +1884,7 @@ static size_t scr_td_utf8_tail(const uint8_t *bytes, size_t n) {
ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *input,
double encoding, bool fatal, bool ignore_bom, bool stream) {
+ SCR_SHARED_GUARD(input, NULL);
if (encoding == 36) encoding = 7; /* ISO-8859-8-I is a single-byte decoder. */
ScrDyn *saved = scr_dyn_obj_read(state, "pending", 7);
size_t pending = saved->kind == SCR_DYN_BYTES ? saved->v.bytes->len : 0;
@@ -1950,6 +1972,7 @@ ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination) {
}
const ScrStr *text = source->v.str;
ScrBytes *dest = destination->v.bytes;
+ SCR_SHARED_GUARD(dest, NULL);
size_t read = 0, written = 0;
size_t limit = text->len < dest->len ? text->len : dest->len;
while (written < limit) {
@@ -1971,7 +1994,7 @@ ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination) {
}
static ScrBytes *scr_buffer_validation_input(const ScrDyn *input) {
- if ((input->kind == SCR_DYN_BYTES && !input->v.bytes->is_data_view) || scr_array_buffer_is(input)) {
+ if ((input->kind == SCR_DYN_BYTES && !input->v.bytes->is_data_view) || scr_buffer_storage_is(input)) {
return scr_bytes_buffer_source(input);
}
scr_dyn_arg_type_fail("input", "an instance of ArrayBuffer, Buffer, or TypedArray", input);
@@ -1986,6 +2009,7 @@ static bool scr_buffer_utf8_valid(const uint8_t *data, size_t len) {
bool scr_buffer_is_ascii(const ScrDyn *input) {
ScrBytes *bytes = scr_buffer_validation_input(input);
if (!bytes) return false;
+ SCR_SHARED_GUARD(bytes, NULL);
bool result = scr_bytes_ascii_prefix(bytes->data, bytes->len) == bytes->len;
scr_bytes_release(bytes);
return result;
@@ -1994,6 +2018,7 @@ bool scr_buffer_is_ascii(const ScrDyn *input) {
bool scr_buffer_is_utf8(const ScrDyn *input) {
ScrBytes *bytes = scr_buffer_validation_input(input);
if (!bytes) return false;
+ SCR_SHARED_GUARD(bytes, NULL);
bool result = scr_buffer_utf8_valid(bytes->data, bytes->len);
scr_bytes_release(bytes);
return result;
@@ -2084,6 +2109,7 @@ ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const S
return NULL;
}
const ScrBytes *input = source->v.bytes;
+ SCR_BYTES_SNAPSHOT(input);
size_t len = input->len;
if (!len) { ScrBytes *empty = scr_bytes_alloc(SCR_BYTES_U8, 0); empty->is_buffer = true; return empty; }
int from_encoding = scr_transcode_encoding(from), to_encoding = scr_transcode_encoding(to);
@@ -2167,6 +2193,7 @@ static ScrStr *scr_bytes_decode_utf16le(const uint8_t *in, size_t n) {
}
ScrStr *scr_bytes_to_str(const ScrBytes *b, const ScrStr *enc) {
+ SCR_SHARED_GUARD(b, NULL);
const uint8_t *in = b->data;
size_t n = b->len; /* u8: len == byte length */
/* Implicit native string/number coercions have no encoding argument. */
@@ -2496,6 +2523,7 @@ bool scr_bytes_validate_off(const char *name, double value, double max) {
}
bool scr_bytes_equals(const ScrBytes *a, const ScrBytes *b) {
+ SCR_SHARED_GUARD(a, b);
return a->len == b->len && memcmp(a->data, b->data, a->len) == 0;
}
@@ -2505,6 +2533,7 @@ bool scr_bytes_equals(const ScrBytes *a, const ScrBytes *b) {
* Node's exactly. */
double scr_bytes_compare(const ScrBytes *src, const ScrBytes *target, double nargs,
double ts, double te, double ss, double se) {
+ SCR_SHARED_GUARD(src, target);
size_t n = (size_t)nargs;
if (n < 1) ts = 0;
else if (!scr_bytes_validate_off("targetStart", ts, 9007199254740991.0)) return 0;
@@ -2533,6 +2562,7 @@ double scr_bytes_compare(const ScrBytes *src, const ScrBytes *target, double nar
* count from the end, and a still-negative backward search answers -1),
* and the utf16le alignment stride. */
double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd) {
+ SCR_SHARED_GUARD(b, needle);
size_t len = b->len, nlen = needle->len;
size_t step = align == 2 ? 2 : 1;
double o = (off != off) ? (fwd ? 0 : (double)len) : trunc(off);
@@ -2563,6 +2593,7 @@ double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off,
}
double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd) {
+ SCR_SHARED_GUARD(b, NULL);
uint8_t byte = (uint8_t)scr_bytes_to_u32(v);
ScrBytes needle = { .rc = 1, .len = 1, .elem = SCR_BYTES_U8, .data = &byte, .backing = NULL };
return scr_bytes_index_of(b, &needle, off, 1, fwd);
@@ -2599,16 +2630,19 @@ static ScrBytes *scr_bytes_fill_core(ScrBytes *b, const uint8_t *pat, size_t pat
}
ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end) {
+ SCR_SHARED_GUARD(b, pattern);
return scr_bytes_fill_core(b, pattern->data, pattern->len, false, nargs, offset, end);
}
ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end) {
+ SCR_SHARED_GUARD(b, NULL);
uint8_t byte = (uint8_t)scr_bytes_to_u32(v);
return scr_bytes_fill_core(b, &byte, 1, false, nargs, offset, end);
}
ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
double nargs, double offset, double end) {
+ SCR_SHARED_GUARD(b, NULL);
ScrBytes *pat = scr_bytes_from_str(s, enc);
ScrBytes *r = scr_bytes_fill_core(b, pat->data, pat->len, true, nargs, offset, end);
scr_bytes_release(pat);
@@ -2621,6 +2655,7 @@ ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
* bounded by the source length. Returns the byte count copied. */
double scr_bytes_copy_into(const ScrBytes *src, ScrBytes *dst, double nargs,
double ts, double ss, double se) {
+ SCR_SHARED_GUARD(src, dst);
size_t n = (size_t)nargs;
ts = n < 1 ? 0 : trunc(ts);
ss = n < 2 ? 0 : trunc(ss);
@@ -2643,6 +2678,7 @@ double scr_bytes_copy_into(const ScrBytes *src, ScrBytes *dst, double nargs,
* (+1, chaining like fill). A length off the width is Node's constant
* ERR_INVALID_BUFFER_SIZE RangeError. */
ScrBytes *scr_bytes_swap(ScrBytes *b, double width) {
+ SCR_SHARED_GUARD(b, NULL);
size_t w = (size_t)width;
if (b->len % w != 0) {
char msg[64];
@@ -2668,6 +2704,7 @@ ScrBytes *scr_bytes_swap(ScrBytes *b, double width) {
* lone lead), byte-level for the rest. Returns the bytes written. */
double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
double offset, double len, bool has_len) {
+ SCR_SHARED_GUARD(b, NULL);
if (!scr_bytes_validate_off("offset", offset, (double)b->len)) return 0;
size_t o = (size_t)offset;
size_t remaining = b->len - o;
@@ -2771,7 +2808,7 @@ ScrBytes *scr_bytes_concat_len(const ScrArr *list, double total) {
return NULL;
}
size_t take = part->len < b->len - o ? part->len : b->len - o;
- memcpy(b->data + o, part->data, take);
+ scr_bytes_read(part, 0, b->data + o, take);
o += take;
scr_bytes_release(part);
}
@@ -2837,7 +2874,7 @@ ScrBytes *scr_bytes_concat(const ScrArr *list) {
scr_bytes_release(b);
return NULL;
}
- memcpy(b->data + o, part->data, part->len);
+ scr_bytes_read(part, 0, b->data + o, part->len);
o += part->len;
scr_bytes_release(part);
}
@@ -2968,6 +3005,7 @@ static size_t scr_bytes_num_width(ScrBytesNumKind kind) {
}
double scr_bytes_read_num(const ScrBytes *b, double offset, ScrBytesNumKind kind, bool le) {
+ SCR_SHARED_GUARD(b, NULL);
size_t width = scr_bytes_num_width(kind);
if (!scr_bytes_rw_check(b, offset, width)) return 0;
const uint8_t *p = b->data + (size_t)offset;
@@ -3002,6 +3040,7 @@ double scr_bytes_read_num(const ScrBytes *b, double offset, ScrBytesNumKind kind
}
double scr_bytes_write_num(ScrBytes *b, double value, double offset, ScrBytesNumKind kind, bool le) {
+ SCR_SHARED_GUARD(b, NULL);
size_t width = scr_bytes_num_width(kind);
uint64_t u;
if (kind == SCR_BN_F32 || kind == SCR_BN_F64) {
@@ -3033,6 +3072,7 @@ double scr_bytes_write_num(ScrBytes *b, double value, double offset, ScrBytesNum
/* readUIntLE/BE / readIntLE/BE — the variable-width family: byteLength
* validates FIRST (1-6, its own error ladder), then the offset gate. */
double scr_bytes_read_var(const ScrBytes *b, double offset, double byte_length, bool sign, bool le) {
+ SCR_SHARED_GUARD(b, NULL);
if (floor(byte_length) != byte_length || byte_length < 1 || byte_length > 6) {
scr_bytes_bounds_error(byte_length, 6, "byteLength");
return 0;
@@ -3053,6 +3093,7 @@ double scr_bytes_read_var(const ScrBytes *b, double offset, double byte_length,
/* writeUIntLE/BE / writeIntLE/BE: byteLength, then value, then offset —
* Node's exact check order. Returns offset + byteLength. */
double scr_bytes_write_var(ScrBytes *b, double value, double offset, double byte_length, bool sign, bool le) {
+ SCR_SHARED_GUARD(b, NULL);
if (floor(byte_length) != byte_length || byte_length < 1 || byte_length > 6) {
scr_bytes_bounds_error(byte_length, 6, "byteLength");
return 0;
@@ -3070,6 +3111,7 @@ double scr_bytes_write_var(ScrBytes *b, double value, double offset, double byte
}
bool scr_bytes_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t *out) {
+ SCR_SHARED_GUARD(b, NULL);
if (!scr_bytes_rw_check(b, offset, 8)) return false;
const uint8_t *p = b->data + (size_t)offset;
uint64_t value = 0;
@@ -3079,6 +3121,7 @@ bool scr_bytes_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t
}
bool scr_bytes_write_u64_raw(ScrBytes *b, double offset, bool le, uint64_t value) {
+ SCR_SHARED_GUARD(b, NULL);
if (!scr_bytes_rw_check(b, offset, 8)) return false;
uint8_t *p = b->data + (size_t)offset;
for (size_t i = 0; i < 8; i++) p[le ? i : 7 - i] = (uint8_t)(value >> (8 * i));
@@ -3096,6 +3139,7 @@ static bool scr_dataview_u64_check(const ScrBytes *b, double byte_off) {
}
bool scr_dataview_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t *out) {
+ SCR_SHARED_GUARD(b, NULL);
double off = (offset != offset) ? 0 : trunc(offset);
if (!scr_dataview_u64_check(b, offset)) return false;
const uint8_t *p = b->data + (size_t)off;
@@ -3106,6 +3150,7 @@ bool scr_dataview_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64
}
bool scr_dataview_write_u64_raw(ScrBytes *b, double offset, bool le, uint64_t value) {
+ SCR_SHARED_GUARD(b, NULL);
double off = (offset != offset) ? 0 : trunc(offset);
if (!scr_dataview_u64_check(b, offset)) return false;
uint8_t *p = b->data + (size_t)off;
diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c
index 8ac1a0126..1454d3086 100644
--- a/packages/runtime/src/scr_bytes_io.c
+++ b/packages/runtime/src/scr_bytes_io.c
@@ -105,6 +105,7 @@ ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc) {
}
static void scr_fs_write_bytes_common(ScrStr *path, const ScrBytes *data, const char *mode) {
+ SCR_BYTES_SNAPSHOT(data);
FILE *f = fopen(path->data, mode);
if (!f) {
scr_fs_throw(errno, "open", path);
@@ -255,7 +256,7 @@ static double scr_fs_cb_open(const ScrStr *path, const ScrDyn *flag, double mode
static ScrStr *scr_fs_cb_path(const ScrDyn *value, const char *name) {
if (!scr_fs_path_chk(value, name)) return NULL;
ScrStr *path = value->kind == SCR_DYN_STR ? scr_str_retain(value->v.str)
- : scr_str_new((const char *)value->v.bytes->data, value->v.bytes->len);
+ : scr_bytes_string(value->v.bytes, 0, value->v.bytes->len);
if (memchr(path->data, 0, path->len)) {
scr_fs_invalid_named_path(value, path, name);
scr_str_release(path);
@@ -973,11 +974,13 @@ ScrBytes *scr_crypto_random_bytes(double n) {
/* ── process.stdout/stderr.write(buf) ──────────────────────────────────── */
bool scr_process_stdout_write_bytes(const ScrBytes *b, const ScrStr *encoding) {
+ SCR_BYTES_SNAPSHOT(b);
(void)encoding;
return scr_stdio_write(1, b->data, b->len * scr_bytes_elem_size(b->elem));
}
bool scr_process_stderr_write_bytes(const ScrBytes *b, const ScrStr *encoding) {
+ SCR_BYTES_SNAPSHOT(b);
(void)encoding;
return scr_stdio_write(2, b->data, b->len * scr_bytes_elem_size(b->elem));
}
@@ -1300,7 +1303,7 @@ static void scr_fs_exists_fire(ScrClosure *self) {
ans = scr_fs_exists(p);
scr_str_release(p);
} else if (scr_dyn_bytes_is(path, SCR_BYTES_U8)) {
- ScrStr *p = scr_str_new((const char *)path->v.bytes->data, path->v.bytes->len);
+ ScrStr *p = scr_bytes_string(path->v.bytes, 0, path->v.bytes->len);
ans = scr_fs_exists(p);
scr_str_release(p);
}
@@ -1449,7 +1452,7 @@ void scr_fs_lchmod_chk(const ScrDyn *path, const ScrDyn *mode, const ScrDyn *cb,
#ifdef __APPLE__
static void scr_fs_lchmod_apply(const ScrDyn *path, const ScrDyn *mode) {
ScrStr *p = path->kind == SCR_DYN_STR ? scr_str_retain(path->v.str)
- : scr_str_new((const char *)path->v.bytes->data, path->v.bytes->len);
+ : scr_bytes_string(path->v.bytes, 0, path->v.bytes->len);
double m = mode->kind == SCR_DYN_NUM ? mode->v.num : (double)strtol(mode->v.str->data, NULL, 8);
if (lchmod(p->data, (mode_t)m) != 0) scr_fs_throw(errno, "lchmod", p);
scr_str_release(p);
diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c
index 23db0c6ee..8f2076de4 100644
--- a/packages/runtime/src/scr_child.c
+++ b/packages/runtime/src/scr_child.c
@@ -1261,9 +1261,9 @@ struct ScrChildWriter {
struct ScrChildWriter *next; /* the service registry (+1) */
};
-static ScrChildStream *scr_child_streams = NULL;
-static size_t scr_child_streams_watching = 0;
-static ScrChildWriter *scr_child_writers = NULL;
+static SCR_TL ScrChildStream *scr_child_streams = NULL;
+static SCR_TL size_t scr_child_streams_watching = 0;
+static SCR_TL ScrChildWriter *scr_child_writers = NULL;
static bool scr_child_writers_pending(void);
static void scr_child_writers_service(void);
static void scr_child_writer_child_settled(ScrChildWriter *w);
@@ -1304,14 +1304,14 @@ struct ScrChild {
struct ScrChild *next; /* the pending registry */
};
-static ScrChild *scr_children = NULL;
-static ScrChild *scr_children_closing = NULL;
-static size_t scr_children_reffed_n = 0;
+static SCR_TL ScrChild *scr_children = NULL;
+static SCR_TL ScrChild *scr_children_closing = NULL;
+static SCR_TL size_t scr_children_reffed_n = 0;
/* Pending children the handle wait can NOT represent — spawn failures
* awaiting their first-pass settle. While any exist, scr_children_wait
* declines and the loop keeps the ~1ms polling cap (the POSIX arm's
* unwatched fallback, exactly). */
-static size_t scr_children_unwatched = 0;
+static SCR_TL size_t scr_children_unwatched = 0;
static void scr_child_drop_terminal_listeners(ScrChild *c) {
scr_child_drop_spawn_listeners(c);
@@ -1685,7 +1685,7 @@ void scr_child_stream_thunk_str(ScrClosure *cb, ScrStr *chunk) {
#define SCR_CHILD_WRITER_HWM 16384
-static const char *scr_child_writer_err_code = NULL;
+static SCR_TL const char *scr_child_writer_err_code = NULL;
static void scr_child_writer_drop_zero_listeners(ScrChildWriter *w) {
for (size_t i = 0; i < w->n_drain; i++) scr_closure_release(w->drain_ls[i].cb);
@@ -1908,6 +1908,7 @@ bool scr_child_writer_write_string(ScrChildWriter *w, const ScrStr *data) {
}
bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
return scr_child_writer_write(w, data->data, data->len);
}
@@ -2431,12 +2432,12 @@ void scr_child_err_thunk0(ScrClosure *cb, ScrStr *msg) {
((void (*)(ScrClosure *))cb->fn)(cb);
}
-static const char *scr_child_err_code = NULL;
+static SCR_TL const char *scr_child_err_code = NULL;
/* The errno name embedded in an errnoException-style message — the
* POSIX arm's parser, shared story (see its comment). */
static const char *scr_err_msg_code(const ScrStr *msg) {
- static char buf[32];
+ static SCR_TL char buf[32];
const char *sp = strchr((const char *)msg->data, ' ');
if (sp == NULL || sp[1] != 'E') return NULL;
const char *tok = sp + 1;
@@ -3541,11 +3542,11 @@ struct ScrChildWriter {
struct ScrChildWriter *next; /* the service registry (+1) */
};
-static ScrChildStream *scr_child_streams = NULL;
+static SCR_TL ScrChildStream *scr_child_streams = NULL;
/* Streams with a live consumer (data listener, not yet EOF): the loop's
* keep-alive and service predicate. */
-static size_t scr_child_streams_watching = 0;
-static ScrChildWriter *scr_child_writers = NULL;
+static SCR_TL size_t scr_child_streams_watching = 0;
+static SCR_TL ScrChildWriter *scr_child_writers = NULL;
static bool scr_child_writers_pending(void);
static void scr_child_writers_service(void);
static void scr_child_writer_child_settled(ScrChildWriter *w);
@@ -3588,18 +3589,18 @@ struct ScrChild {
struct ScrChild *next; /* the pending registry */
};
-static ScrChild *scr_children = NULL; /* unsettled children (registry +1) */
-static ScrChild *scr_children_closing = NULL; /* exited, waiting for stdio EOF */
-static size_t scr_children_reffed_n = 0; /* unsettled AND reffed (liveness) */
+static SCR_TL ScrChild *scr_children = NULL; /* unsettled children (registry +1) */
+static SCR_TL ScrChild *scr_children_closing = NULL; /* exited, waiting for stdio EOF */
+static SCR_TL size_t scr_children_reffed_n = 0; /* unsettled AND reffed (liveness) */
/* Children the kqueue can NOT wake the loop for (spawn failures awaiting
* their first-pass settle, exit-filter registration failures, every child
* on a non-kqueue platform): while any exist, scr_children_wait refuses
* and the loop keeps the polling cap for them. */
-static size_t scr_children_unwatched = 0;
+static SCR_TL size_t scr_children_unwatched = 0;
#ifdef SCR_HAVE_KQUEUE
-static int scr_child_kq = -1; /* created with the first spawn; lives forever */
+static SCR_TL int scr_child_kq = -1; /* created with the first spawn; lives forever */
/* Arms NOTE_EXIT for a freshly spawned pid. False = the child cannot be
* watched (it already exited and EVFILT_PROC refuses zombies — the very
@@ -3622,7 +3623,7 @@ static void scr_child_unwatch(pid_t pid) { (void)pid; }
#endif
#ifdef SCR_CHILD_HAVE_PIDFD
-static int scr_child_ep = -1; /* created with the first spawn; lives forever */
+static SCR_TL int scr_child_ep = -1; /* created with the first spawn; lives forever */
/* pid -> pidfd, so the reap can CLOSE the pidfd (it polls readable from
* exit until closed — kqueue's ONESHOT drop has no epoll analogue). A
@@ -3632,8 +3633,8 @@ typedef struct {
int pidfd;
} ScrChildPidfd;
-static ScrChildPidfd *scr_child_pidfds = NULL;
-static size_t scr_child_pidfds_n = 0, scr_child_pidfds_cap = 0;
+static SCR_TL ScrChildPidfd *scr_child_pidfds = NULL;
+static SCR_TL size_t scr_child_pidfds_n = 0, scr_child_pidfds_cap = 0;
/* Arms an exit wakeup for a freshly spawned pid: pidfd_open registered
* EPOLLIN. False = the child cannot be watched (ENOSYS on pre-5.3
@@ -4225,7 +4226,7 @@ void scr_child_stream_thunk_str(ScrClosure *cb, ScrStr *chunk) {
#define SCR_CHILD_WRITER_HWM 65536
-static const char *scr_child_writer_err_code = NULL;
+static SCR_TL const char *scr_child_writer_err_code = NULL;
static void scr_child_writer_drop_zero_listeners(ScrChildWriter *w) {
for (size_t i = 0; i < w->n_drain; i++) scr_closure_release(w->drain_ls[i].cb);
@@ -4432,6 +4433,7 @@ bool scr_child_writer_write_string(ScrChildWriter *w, const ScrStr *data) {
}
bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
return scr_child_writer_write(w, data->data, data->len);
}
@@ -4970,6 +4972,22 @@ void scr_children_teardown(void) {
scr_child_drop_listeners(c);
scr_child_release(c);
}
+#ifdef SCR_WORKERS
+ /* Watchers belong to the script context, which can end long before the
+ * process. Release their OS handles and reusable pid index at loop exit. */
+#ifdef SCR_HAVE_KQUEUE
+ if (scr_child_kq >= 0) close(scr_child_kq);
+ scr_child_kq = -1;
+#endif
+#ifdef SCR_CHILD_HAVE_PIDFD
+ for (size_t i = 0; i < scr_child_pidfds_n; i++) close(scr_child_pidfds[i].pidfd);
+ free(scr_child_pidfds);
+ scr_child_pidfds = NULL;
+ scr_child_pidfds_n = scr_child_pidfds_cap = 0;
+ if (scr_child_ep >= 0) close(scr_child_ep);
+ scr_child_ep = -1;
+#endif
+#endif
}
void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn) {
@@ -5039,7 +5057,7 @@ void scr_child_err_thunk0(ScrClosure *cb, ScrStr *msg) {
* Node's `code` on the constructed error (the thunk signature is shared
* with scr_net.c's error events, which parse theirs out of the message
* below). NULL outside a spawn-failure settle. */
-static const char *scr_child_err_code = NULL;
+static SCR_TL const char *scr_child_err_code = NULL;
/* [.code on event errors — net/http/dgram parity with spawn failures]
* The errno name embedded in an errnoException-style message ("connect
@@ -5054,7 +5072,7 @@ static const char *scr_child_err_code = NULL;
* valid until the next call, or NULL for code-less messages ("socket
* hang up" stays undefined — divergence 54's documented bound). */
static const char *scr_err_msg_code(const ScrStr *msg) {
- static char buf[32];
+ static SCR_TL char buf[32];
const char *sp = strchr((const char *)msg->data, ' ');
if (sp == NULL || sp[1] != 'E') return NULL;
const char *tok = sp + 1;
@@ -5541,10 +5559,10 @@ struct ScrIpc {
struct ScrIpc *next;
};
-static ScrIpc *scr_ipcs = NULL;
-static ScrIpc *scr_process_ipc = NULL;
-static double scr_process_fork_id = -2;
-static bool scr_ipc_cleanup_registered = false;
+static SCR_TL ScrIpc *scr_ipcs = NULL;
+static SCR_TL ScrIpc *scr_process_ipc = NULL;
+static SCR_TL double scr_process_fork_id = -2;
+static SCR_TL bool scr_ipc_cleanup_registered = false;
static ScrIpc *scr_ipc_retain(ScrIpc *ipc) {
if (ipc && ipc->rc != SIZE_MAX) ipc->rc++;
@@ -5633,7 +5651,7 @@ static void scr_ipc_stream_data(ScrClosure *closure, ScrBytes *chunk) {
if (!ipc->buffer) scr_trap("out of memory");
ipc->buffer_cap = cap;
}
- memcpy(ipc->buffer + ipc->buffer_len, chunk->data, chunk->len);
+ scr_bytes_read(chunk, 0, ipc->buffer + ipc->buffer_len, chunk->len);
ipc->buffer_len = need;
size_t consumed = 0;
for (size_t i = 0; i < ipc->buffer_len; i++) {
@@ -5705,7 +5723,7 @@ static ScrIpc *scr_ipc_new(ScrChildStream *reader, ScrChildWriter *writer) {
&scr_ipc_writer_error, false);
if (!scr_ipc_cleanup_registered) {
scr_ipc_cleanup_registered = true;
- atexit(scr_ipc_global_cleanup);
+ scr_atexit(scr_ipc_global_cleanup);
}
return ipc;
}
diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c
index 87d7e5fa5..abe9ef0b1 100644
--- a/packages/runtime/src/scr_console.c
+++ b/packages/runtime/src/scr_console.c
@@ -107,9 +107,12 @@ static void scr_enable_utf8_console_output(void) {
}
#endif
-void scr_runtime_abi_v6(void) {}
+void scr_runtime_abi_v7(void) {}
void scr_init(void) {
+#ifdef SCR_WORKERS
+ if (scr_context_is_main()) {
+#endif
#ifdef _WIN32
/* The CRT opens std streams in TEXT mode, which writes \n as \r\n. Node
* on Windows does NOT translate — console.log emits \n whether stdout is
@@ -133,16 +136,19 @@ void scr_init(void) {
* stream immediately. */
static char outbuf[1 << 16];
setvbuf(stdout, outbuf, _IOFBF, sizeof outbuf);
+#ifdef SCR_WORKERS
+ }
+#endif
/* atexit is LIFO; registration order makes exit run: library cleanup
* (registered later, in scr_lib_init) → cycle collection → RC audit →
* flush → Windows console-code-page restore. The final collection frees
* cycles the program dropped, so the audit (and ASan's leak check) see
* them as freed, not leaked. */
- atexit(scr_flush_at_exit);
+ scr_atexit(scr_flush_at_exit);
#ifdef SCR_RC_AUDIT
- atexit(scr_rc_audit_at_exit);
+ scr_atexit(scr_rc_audit_at_exit);
#endif
- atexit(scr_collect_cycles_at_exit);
+ scr_atexit(scr_collect_cycles_at_exit);
}
#endif /* !SCR_LIB — a library artifact never touches host stdio modes/buffering and
* registers no atexit handlers: scr_init and its exit hooks (the
@@ -154,7 +160,7 @@ void scr_init(void) {
* the stream differs. */
/* A real definition lets relocatable library links localize this hook;
* a tentative common symbol stays externally visible on Mach-O. */
-bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len) = NULL;
+SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len) = NULL;
static void scr_console_write(FILE *out, size_t n, const ScrLogArg *args) {
if (scr_stdio_write_hook) {
@@ -192,6 +198,15 @@ static void scr_console_write(FILE *out, size_t n, const ScrLogArg *args) {
if (scr_exc_pending()) scr_exc_clear();
return;
}
+ /* Formatting a single line uses several libc calls. Hold the stream lock
+ * for that whole chunk so concurrent workers cannot interleave arguments. */
+#ifdef SCR_WORKERS
+#ifdef _WIN32
+ _lock_file(out);
+#else
+ flockfile(out);
+#endif
+#endif
char numbuf[32];
for (size_t i = 0; i < n; i++) {
if (i > 0) fputc(' ', out);
@@ -221,6 +236,13 @@ static void scr_console_write(FILE *out, size_t n, const ScrLogArg *args) {
* Keep the C buffer only as a formatter coalescing detail: a caller
* observing a live child must see this line before the next JS turn. */
fflush(out);
+#ifdef SCR_WORKERS
+#ifdef _WIN32
+ _unlock_file(out);
+#else
+ funlockfile(out);
+#endif
+#endif
}
void scr_console_log(size_t n, const ScrLogArg *args) {
diff --git a/packages/runtime/src/scr_context.c b/packages/runtime/src/scr_context.c
new file mode 100644
index 000000000..bb735bc1b
--- /dev/null
+++ b/packages/runtime/src/scr_context.c
@@ -0,0 +1,167 @@
+#include "scr_runtime.h"
+
+#ifdef SCR_WORKERS
+#include
+#include
+
+void scr_runtime_workers_v7(void) {}
+
+typedef struct ScrContextExit {
+ void (*fn)(void);
+ struct ScrContextExit *next;
+} ScrContextExit;
+
+static SCR_TL ScrContextExit *scr_context_exits;
+static SCR_TL uint64_t scr_context_id;
+static SCR_TL const atomic_bool *scr_context_cancel;
+static SCR_TL bool scr_context_stopped;
+SCR_TL void (*scr_context_report_error)(void);
+
+typedef struct { char *name; char *value; } ScrContextEnvEntry;
+struct ScrContextEnv { size_t count; ScrContextEnvEntry *entries; };
+static SCR_TL ScrContextEnv *scr_context_environment;
+
+static char *scr_context_text_copy(const char *value) {
+ size_t length = strlen(value);
+ char *copy = malloc(length + 1);
+ if (!copy) scr_trap("scriptc: out of memory\n");
+ memcpy(copy, value, length + 1);
+ return copy;
+}
+
+ScrContextEnv *scr_context_env_capture(void) {
+ ScrArr *pairs = scr_env_pairs();
+ ScrContextEnv *environment = calloc(1, sizeof(*environment));
+ if (!environment) scr_trap("scriptc: out of memory\n");
+ environment->count = pairs->len / 2;
+ environment->entries = calloc(environment->count ? environment->count : 1, sizeof(*environment->entries));
+ if (!environment->entries) scr_trap("scriptc: out of memory\n");
+ for (size_t i = 0; i < environment->count; i++) {
+ ScrStr *key = scr_arr_get_ref(pairs, (double)(2 * i));
+ ScrStr *value = scr_arr_get_ref(pairs, (double)(2 * i + 1));
+ environment->entries[i] = (ScrContextEnvEntry){scr_context_text_copy(key->data), scr_context_text_copy(value->data)};
+ scr_str_release(key); scr_str_release(value);
+ }
+ scr_arr_release(pairs);
+ return environment;
+}
+
+void scr_context_env_free(ScrContextEnv *environment) {
+ if (!environment) return;
+ for (size_t i = 0; i < environment->count; i++) {
+ free(environment->entries[i].name); free(environment->entries[i].value);
+ }
+ free(environment->entries); free(environment);
+}
+
+static void scr_context_env_cleanup(void) {
+ scr_context_env_free(scr_context_environment);
+ scr_context_environment = NULL;
+}
+
+void scr_context_env_enter(ScrContextEnv *environment) {
+ scr_context_environment = environment;
+ if (scr_context_atexit(scr_context_env_cleanup)) scr_trap("scriptc: out of memory\n");
+}
+
+const char *scr_context_getenv(const char *name) {
+ if (!scr_context_environment) return getenv(name);
+ for (size_t i = 0; i < scr_context_environment->count; i++)
+ if (!strcmp(scr_context_environment->entries[i].name, name)) return scr_context_environment->entries[i].value;
+ return NULL;
+}
+
+bool scr_context_env_set(const char *name, const char *value) {
+ ScrContextEnv *environment = scr_context_environment;
+ if (!environment) return false;
+ for (size_t i = 0; i < environment->count; i++) {
+ ScrContextEnvEntry *entry = &environment->entries[i];
+ if (strcmp(entry->name, name)) continue;
+ char *copy = value ? scr_context_text_copy(value) : NULL;
+ free(entry->value);
+ entry->value = copy;
+ if (!copy) {
+ free(entry->name);
+ memmove(entry, entry + 1, (--environment->count - i) * sizeof(*entry));
+ }
+ return true;
+ }
+ if (value) {
+ ScrContextEnvEntry *entries = realloc(environment->entries, (environment->count + 1) * sizeof(*entries));
+ if (!entries) scr_trap("scriptc: out of memory\n");
+ environment->entries = entries;
+ entries[environment->count++] = (ScrContextEnvEntry){scr_context_text_copy(name), scr_context_text_copy(value)};
+ }
+ return true;
+}
+
+ScrArr *scr_context_env_pairs(void) {
+ if (!scr_context_environment) return NULL;
+ ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0);
+ for (size_t i = 0; i < scr_context_environment->count; i++) {
+ const ScrContextEnvEntry *entry = &scr_context_environment->entries[i];
+ scr_arr_push_ref(out, scr_str_new(entry->name, strlen(entry->name)));
+ scr_arr_push_ref(out, scr_str_new(entry->value, strlen(entry->value)));
+ }
+ return out;
+}
+
+void scr_context_enter(uint64_t thread_id) { scr_context_id = thread_id; }
+bool scr_context_is_main(void) { return scr_context_id == 0; }
+uint64_t scr_context_thread_id(void) { return scr_context_id; }
+double scr_context_thread_number(void) { return (double)scr_context_id; }
+
+void scr_context_stop_flag(const void *flag) { scr_context_cancel = flag; }
+bool scr_context_stopping(void) { return scr_context_stopped; }
+
+void scr_context_stop(int code) {
+ scr_context_stopped = true;
+ scr_exit_code_note(code);
+ scr_exc_clear();
+ scr_exc_current_cell()->kind = SCR_EXC_TERMINATE;
+}
+
+bool scr_context_checkpoint(void) {
+ if (!scr_context_stopped && scr_context_cancel &&
+ atomic_load_explicit(scr_context_cancel, memory_order_relaxed)) scr_context_stop(1);
+ if (!scr_context_stopped) return false;
+ /* Each fiber has its own exception cell. Reinstall the sentinel after a
+ * context switch or a runtime continuation consumes its pending payload. */
+ if (scr_exc_current_cell()->kind != SCR_EXC_TERMINATE) {
+ scr_exc_clear();
+ scr_exc_current_cell()->kind = SCR_EXC_TERMINATE;
+ }
+ return true;
+}
+
+int scr_context_atexit(void (*fn)(void)) {
+ ScrContextExit *entry = malloc(sizeof(*entry));
+ if (!entry) return -1;
+ entry->fn = fn;
+ entry->next = scr_context_exits;
+ scr_context_exits = entry;
+ return 0;
+}
+
+void scr_context_cleanup(void) {
+ scr_loop_context_shutdown();
+ /* Pop before calling: a callback may register further cleanup. Those
+ * registrations run first, just as newly registered libc exit handlers do.
+ * Repeated cleanup is harmless after the list has been drained. */
+ while (scr_context_exits) {
+ ScrContextExit *entry = scr_context_exits;
+ scr_context_exits = entry->next;
+ void (*fn)(void) = entry->fn;
+ free(entry);
+ fn();
+ }
+ scr_exc_clear();
+ scr_cyc_context_cleanup();
+}
+#else
+bool scr_context_is_main(void) { return true; }
+double scr_context_thread_number(void) { return 0; }
+double scr_worker_root(void) { return -1; }
+ScrDyn *scr_worker_data(void) { return scr_dyn_new_null(); }
+ScrDyn *scr_worker_parent_port(void) { return scr_dyn_new_null(); }
+#endif
diff --git a/packages/runtime/src/scr_context.test.c b/packages/runtime/src/scr_context.test.c
new file mode 100644
index 000000000..32276b43f
--- /dev/null
+++ b/packages/runtime/src/scr_context.test.c
@@ -0,0 +1,118 @@
+#undef NDEBUG
+#include "scr_async.c"
+#include
+#include
+#include
+
+enum { CONTEXTS = 4, REQUESTS = 64 };
+static atomic_uint started;
+static SCR_TL unsigned completed;
+static SCR_TL unsigned destroyed;
+static SCR_TL unsigned cleanup_order;
+static SCR_TL unsigned timer_calls;
+
+typedef struct Work {
+ uint64_t owner;
+ unsigned input;
+ unsigned result;
+ ScrStr *text;
+ bool deliver;
+} Work;
+
+static void run_work(void *data) {
+ Work *work = data;
+ work->result = work->input * 7 + 3;
+}
+
+static void deliver_work(void *data) {
+ Work *work = data;
+ assert(work->deliver);
+ assert(scr_context_thread_id() == work->owner);
+ assert(work->result == work->input * 7 + 3);
+ completed++;
+}
+
+static void destroy_work(void *data) {
+ Work *work = data;
+ assert(scr_context_thread_id() == work->owner);
+ scr_str_release(work->text);
+ destroyed++;
+ free(work);
+}
+
+static void submit_work(unsigned input, bool deliver) {
+ Work *work = malloc(sizeof(*work));
+ assert(work);
+ *work = (Work){scr_context_thread_id(), input, 0, scr_str_new("owned", 5), deliver};
+ scr_work_submit(work, run_work, deliver_work, destroy_work);
+}
+
+static void timer(ScrClosure *closure) {
+ (void)closure;
+ timer_calls++;
+}
+
+static void cleanup_first(void) {
+ assert(cleanup_order == 2);
+ cleanup_order = 3;
+}
+
+static void cleanup_inserted(void) {
+ assert(cleanup_order == 1);
+ cleanup_order = 2;
+}
+
+static void cleanup_second(void) {
+ assert(cleanup_order == 0);
+ cleanup_order = 1;
+ assert(scr_context_atexit(cleanup_inserted) == 0);
+}
+
+static void *run_context(void *data) {
+ uint64_t id = (uintptr_t)data;
+ scr_context_enter(id);
+ assert(!scr_context_is_main());
+ scr_init();
+ scr_lib_init(0, NULL);
+ assert(scr_context_atexit(cleanup_first) == 0);
+ assert(scr_context_atexit(cleanup_second) == 0);
+
+ /* Leave a distinct exception pending while every context is active. */
+ scr_throw_f64((double)id);
+ atomic_fetch_add_explicit(&started, 1, memory_order_release);
+ while (atomic_load_explicit(&started, memory_order_acquire) != CONTEXTS) sched_yield();
+ ScrCaught *caught = scr_exc_take();
+ assert(caught && caught->kind == SCR_EXC_F64 && caught->f64 == (double)id);
+ scr_caught_release(caught);
+
+ for (unsigned i = 0; i < REQUESTS; i++) submit_work(i, true);
+ ScrClosure *callback = scr_closure_new((void *)timer, 0);
+ scr_set_timeout(callback, 1);
+ assert(!scr_loop_run(NULL));
+ assert(!scr_exc_pending());
+ assert(completed == REQUESTS && destroyed == REQUESTS && timer_calls == 1);
+
+ /* Teardown must wait for borrowed native work, release it on its owner,
+ * and suppress its script callback. It must not stop another context. */
+ submit_work(REQUESTS, false);
+ scr_context_cleanup();
+ assert(destroyed == REQUESTS + 1 && completed == REQUESTS);
+ assert(cleanup_order == 3);
+ scr_context_cleanup();
+ assert(cleanup_order == 3);
+ return NULL;
+}
+
+int main(void) {
+ scr_init();
+ scr_lib_init(0, NULL);
+ pthread_t threads[CONTEXTS];
+ for (uintptr_t i = 0; i < CONTEXTS; i++)
+ assert(pthread_create(&threads[i], NULL, run_context, (void *)(i + 1)) == 0);
+ for (unsigned i = 0; i < CONTEXTS; i++) assert(pthread_join(threads[i], NULL) == 0);
+ assert(scr_context_is_main());
+ assert(!scr_exc_pending());
+ assert(completed == 0 && destroyed == 0 && timer_calls == 0);
+ scr_context_cleanup();
+ puts("runtime contexts keep completions, timers, exceptions and cleanup isolated");
+}
diff --git a/packages/runtime/src/scr_context.test.ts b/packages/runtime/src/scr_context.test.ts
new file mode 100644
index 000000000..83cfdad6b
--- /dev/null
+++ b/packages/runtime/src/scr_context.test.ts
@@ -0,0 +1,47 @@
+import { execFile } from "node:child_process";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { promisify } from "node:util";
+import { expect, test } from "vitest";
+import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js";
+
+const exec = promisify(execFile);
+
+test.skipIf(process.platform === "win32")(
+ "concurrent runtime contexts deliver native work and drain cleanup on their owner",
+ async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-context-"));
+ try {
+ const bin = join(dir, "contexts");
+ await exec("clang", [
+ "-std=c11",
+ "-O1",
+ "-Wall",
+ "-Wextra",
+ "-Wno-deprecated-declarations",
+ "-pthread",
+ "-DSCR_WORKERS",
+ "-DSCR_RC_AUDIT",
+ "-fsanitize=address,undefined",
+ ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
+ join(import.meta.dirname, "scr_context.test.c"),
+ ...EXECUTABLE_RUNTIME_SOURCES.filter((name) => name !== "scr_async.c").map((name) =>
+ join(import.meta.dirname, name),
+ ),
+ ...(process.platform === "linux" ? ["-lm"] : []),
+ "-o",
+ bin,
+ ]);
+ const result = await exec(bin, [], {
+ env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" },
+ });
+ expect(result.stdout).toBe(
+ "runtime contexts keep completions, timers, exceptions and cleanup isolated\n",
+ );
+ expect(result.stderr).toBe("");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ },
+);
diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c
index 61f4a808c..3ff0b5a68 100644
--- a/packages/runtime/src/scr_copying.c
+++ b/packages/runtime/src/scr_copying.c
@@ -124,6 +124,7 @@ ScrArr *scr_arr_with_undefined(ScrArr *a, double index) {
}
ScrBytes *scr_bytes_to_reversed(const ScrBytes *b) {
+ SCR_SHARED_GUARD(b, NULL);
ScrBytes *out = scr_bytes_new(b->elem, (double)b->len);
size_t width = scr_bytes_elem_size(b->elem);
/* Copy the stored representation without numeric conversion. Views may
diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c
index 95fb2c293..3faaed5c5 100644
--- a/packages/runtime/src/scr_cycle.c
+++ b/packages/runtime/src/scr_cycle.c
@@ -244,7 +244,7 @@ static SCR_TL unsigned scr_cyc_backlog_shift = 0;
static size_t scr_cyc_nursery_threshold(void) {
static SCR_TL size_t cached = 0;
if (cached == 0) {
- const char *env = getenv("SCR_CYCLE_THRESHOLD");
+ const char *env = scr_getenv("SCR_CYCLE_THRESHOLD");
long v = env ? strtol(env, NULL, 10) : 0;
cached = v > 0 ? (size_t)v : SCR_CYC_NURSERY_CANDIDATES;
}
@@ -675,3 +675,15 @@ void scr_collect_cycles(void) {
&& (scr_roots[SCR_CYC_NURSERY].n || scr_roots[SCR_CYC_MATURE].n)) {
}
}
+
+/* Worker TLS becomes unreachable when its OS thread returns. Release the
+ * reusable traversal buffers after all context-owned roots are retired. */
+void scr_cyc_context_cleanup(void) {
+ scr_collect_cycles();
+ ScrVec *vectors[] = {&scr_roots[SCR_CYC_NURSERY], &scr_roots[SCR_CYC_MATURE],
+ &scr_cands, &scr_promote, &scr_pending, &scr_restore_pending, &scr_white, &scr_xgen};
+ for (size_t i = 0; i < sizeof vectors / sizeof vectors[0]; i++) {
+ free(vectors[i]->v);
+ *vectors[i] = (ScrVec){0};
+ }
+}
diff --git a/packages/runtime/src/scr_dc.c b/packages/runtime/src/scr_dc.c
index c22c2087a..148d4f3da 100644
--- a/packages/runtime/src/scr_dc.c
+++ b/packages/runtime/src/scr_dc.c
@@ -42,9 +42,9 @@ typedef struct {
size_t nbinds, binds_cap;
} ScrDcChannel;
-static ScrDcChannel *dc_channels = NULL;
-static size_t dc_nchannels = 0, dc_cap = 0;
-static bool dc_teardown_registered = false;
+static SCR_TL ScrDcChannel *dc_channels = NULL;
+static SCR_TL size_t dc_nchannels = 0, dc_cap = 0;
+static SCR_TL bool dc_teardown_registered = false;
static void scr_dc_teardown(void) {
for (size_t i = 0; i < dc_nchannels; i++) {
@@ -75,7 +75,7 @@ static size_t dc_intern(ScrStr *name) {
}
if (!dc_teardown_registered) {
dc_teardown_registered = true;
- atexit(scr_dc_teardown);
+ scr_atexit(scr_dc_teardown);
}
ScrDcChannel *ch = &dc_channels[dc_nchannels];
ch->name = scr_str_retain(name);
@@ -358,9 +358,9 @@ enum { DC_TC_START, DC_TC_END, DC_TC_ASYNC_START, DC_TC_ASYNC_END, DC_TC_ERROR }
static const char *const dc_trace_events[5] = {"start", "end", "asyncStart", "asyncEnd", "error"};
-static ScrDcTracing *dc_tracings = NULL;
-static size_t dc_ntracings = 0, dc_tracings_cap = 0;
-static bool dc_tracings_teardown_registered = false;
+static SCR_TL ScrDcTracing *dc_tracings = NULL;
+static SCR_TL size_t dc_ntracings = 0, dc_tracings_cap = 0;
+static SCR_TL bool dc_tracings_teardown_registered = false;
static void scr_dc_tracings_teardown(void) {
free(dc_tracings);
@@ -379,7 +379,7 @@ static double dc_tracing_register(const size_t ch[5]) {
}
if (!dc_tracings_teardown_registered) {
dc_tracings_teardown_registered = true;
- atexit(scr_dc_tracings_teardown);
+ scr_atexit(scr_dc_tracings_teardown);
}
for (size_t i = 0; i < 5; i++) dc_tracings[dc_ntracings].ch[i] = ch[i];
dc_ntracings++;
diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c
index bdb8875fc..f6eff318d 100644
--- a/packages/runtime/src/scr_dgram.c
+++ b/packages/runtime/src/scr_dgram.c
@@ -346,12 +346,12 @@ struct ScrDgramSocket {
};
#ifdef SCR_RC_AUDIT
-static long scr_dgram_live = 0;
+static SCR_TL long scr_dgram_live = 0;
long scr_dgram_live_count(void) { return scr_dgram_live; }
#endif
-static ScrDgramSocket *scr_dgram_socks = NULL; /* registry: +1 each, tail-appended */
-static ScrPoller *scr_dgram_poller = NULL;
+static SCR_TL ScrDgramSocket *scr_dgram_socks = NULL; /* registry: +1 each, tail-appended */
+static SCR_TL ScrPoller *scr_dgram_poller = NULL;
/* Pending dns.lookup deliveries (already resolved — getaddrinfo ran at
* the call; the sweep fires them FIFO). */
@@ -364,7 +364,7 @@ typedef struct ScrDnsPending {
struct ScrDnsPending *next;
} ScrDnsPending;
-static ScrDnsPending *scr_dns_pending = NULL;
+static SCR_TL ScrDnsPending *scr_dns_pending = NULL;
/* ── RC ──────────────────────────────────────────────────────────────── */
@@ -645,6 +645,7 @@ void scr_dgram_send_str(ScrDgramSocket *s, ScrStr *data, double port, ScrStr *ho
}
void scr_dgram_send_bytes(ScrDgramSocket *s, ScrBytes *data, double port, ScrStr *host) {
+ SCR_BYTES_SNAPSHOT(data);
scr_dgram_send_raw(s, (const char *)data->data, data->len, port, host);
}
@@ -1011,10 +1012,10 @@ static void scr_dgram_cleanup_atexit(void) {
}
void scr_dgram_install(void) {
- static bool installed = false;
+ static SCR_TL bool installed = false;
if (installed) return;
installed = true;
- atexit(scr_dgram_cleanup_atexit);
+ scr_atexit(scr_dgram_cleanup_atexit);
scr_loop_set_dgram(&scr_dgram_pending, &scr_dgram_dispatch, &scr_dgram_pollfd);
}
@@ -1167,8 +1168,10 @@ void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a
scr_throw_lowering_fence(fence);
return;
}
+ const ScrBytes *byte_input = buffer->kind == SCR_DYN_BYTES ? buffer->v.bytes : NULL;
+ SCR_BYTES_SNAPSHOT(byte_input);
const char *data = buffer->kind == SCR_DYN_STR ? buffer->v.str->data
- : (const char *)buffer->v.bytes->data;
+ : (const char *)byte_input->data;
size_t datalen = buffer->kind == SCR_DYN_STR ? buffer->v.str->len
: (size_t)scr_bytes_byte_len(buffer->v.bytes);
if (sliced) {
diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c
index f4e976913..f43adf552 100644
--- a/packages/runtime/src/scr_events.c
+++ b/packages/runtime/src/scr_events.c
@@ -185,6 +185,7 @@ static bool scr_signal_check(ScrStr *name, ScrDyn *cb) {
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once) {
if (!scr_signal_check(name, cb)) return;
+ if (!scr_context_is_main()) return;
#ifdef __wasi__
(void)once;
scr_trap("scriptc: internal error: OS signal surface reached on WASI\n");
@@ -262,6 +263,7 @@ static void scr_sig_remove(ScrSigReg *reg, size_t i) {
void scr_signal_off(ScrStr *name, ScrDyn *cb) {
if (!scr_signal_check(name, cb)) return;
+ if (!scr_context_is_main()) return;
int sig = scr_signal_from_name(name);
if (sig < 0) sig = 0;
ScrSigReg *reg = scr_sig_regs;
@@ -359,20 +361,20 @@ typedef struct {
bool once;
} ScrStdinErrL;
-static ScrStdinDataL *scr_stdin_data = NULL;
-static size_t scr_stdin_ndata = 0, scr_stdin_data_cap = 0;
-static ScrStdinEndL *scr_stdin_end = NULL;
-static size_t scr_stdin_nend = 0, scr_stdin_end_cap = 0;
-static ScrStdinErrL *scr_stdin_err = NULL;
-static size_t scr_stdin_nerr = 0, scr_stdin_err_cap = 0;
-static ScrPromise *scr_stdin_waiter = NULL; /* parked for-await next() */
-static bool scr_stdin_eof = false;
-static bool scr_stdin_destroyed = false;
-static bool scr_stdin_paused = false;
-static bool scr_stdin_resumed = false;
-static bool scr_stdin_read_eof = false;
-static ScrBytes *scr_stdin_buffer;
-static size_t scr_stdin_read_demand;
+static SCR_TL ScrStdinDataL *scr_stdin_data = NULL;
+static SCR_TL size_t scr_stdin_ndata = 0, scr_stdin_data_cap = 0;
+static SCR_TL ScrStdinEndL *scr_stdin_end = NULL;
+static SCR_TL size_t scr_stdin_nend = 0, scr_stdin_end_cap = 0;
+static SCR_TL ScrStdinErrL *scr_stdin_err = NULL;
+static SCR_TL size_t scr_stdin_nerr = 0, scr_stdin_err_cap = 0;
+static SCR_TL ScrPromise *scr_stdin_waiter = NULL; /* parked for-await next() */
+static SCR_TL bool scr_stdin_eof = false;
+static SCR_TL bool scr_stdin_destroyed = false;
+static SCR_TL bool scr_stdin_paused = false;
+static SCR_TL bool scr_stdin_resumed = false;
+static SCR_TL bool scr_stdin_read_eof = false;
+static SCR_TL ScrBytes *scr_stdin_buffer;
+static SCR_TL size_t scr_stdin_read_demand;
void scr_stdin_on_data(ScrClosure *cb /*moves*/, void (*fn)(ScrClosure *, ScrBytes *),
bool once) {
@@ -682,15 +684,15 @@ typedef struct {
ScrDyn *write_error;
} ScrStdio;
-static ScrStdio scr_stdio_streams[3] = {{.fd = 0}, {.fd = 1}, {.fd = 2}};
+static SCR_TL ScrStdio scr_stdio_streams[3] = {{.fd = 0}, {.fd = 1}, {.fd = 2}};
enum { STDIO_WRITE, STDIO_ON, STDIO_ONCE, STDIO_OFF, STDIO_PAUSE, STDIO_RESUME,
STDIO_IS_PAUSED, STDIO_READ, STDIO_RAW, STDIO_DESTROY, STDIO_METHOD_COUNT };
static const char *const scr_stdio_names[] = {
"write", "on", "once", "removeListener", "pause", "resume", "isPaused", "read", "setRawMode", "destroy"
};
-static ScrDyn *scr_stdio_methods[STDIO_METHOD_COUNT];
-static bool scr_stdio_initialized;
-static bool scr_stdio_raw;
+static SCR_TL ScrDyn *scr_stdio_methods[STDIO_METHOD_COUNT];
+static SCR_TL bool scr_stdio_initialized;
+static SCR_TL bool scr_stdio_raw;
static ScrDyn *scr_stdio_refusal(const char *name) {
char message[192];
@@ -868,7 +870,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
bytes = scr_bytes_from_str(arg->v.str, enc);
scr_str_release(enc);
} else if (arg->kind == SCR_DYN_BYTES && arg->v.bytes->elem == SCR_BYTES_U8) {
- bytes = scr_bytes_retain(arg->v.bytes);
+ bytes = arg->v.bytes->shared ? scr_bytes_copy(arg->v.bytes) : scr_bytes_retain(arg->v.bytes);
} else {
scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", arg);
goto done;
@@ -1091,20 +1093,29 @@ typedef struct {
bool once;
} ScrExitListener;
-static ScrExitListener *scr_exit_ls = NULL;
-static size_t scr_exit_n = 0, scr_exit_cap = 0;
-static bool scr_exit_ran = false;
+static SCR_TL ScrExitListener *scr_exit_ls = NULL;
+static SCR_TL size_t scr_exit_n = 0, scr_exit_cap = 0;
+static SCR_TL bool scr_exit_ran = false;
void scr_run_exit_listeners(double code) {
if (scr_exit_ran) return; /* process.exit inside a listener: no recursion */
scr_exit_ran = true;
scr_process_in_exit = true; /* process._exiting — Node's flag */
for (size_t i = 0; i < scr_exit_n; i++) {
+#ifdef SCR_WORKERS
+ if (scr_context_stopping()) break;
+#endif
scr_exit_ls[i].fn(scr_exit_ls[i].cb, code);
+#ifdef SCR_WORKERS
+ if (scr_context_stopping()) break;
+#endif
if (scr_exc_pending()) {
/* Node: a throw in an 'exit' listener is fatal (exit code 7). */
scr_exc_print_uncaught();
fflush(stdout);
+#ifdef SCR_WORKERS
+ if (!scr_context_is_main()) { scr_context_stop(7); break; }
+#endif
_Exit(7);
}
}
@@ -1172,14 +1183,16 @@ void scr_stdin_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk) {
static bool scr_events_pending(void) { return scr_stdin_pending(); }
static bool scr_events_watching(void) {
- return scr_sig_watched > 0 || scr_stdin_pending();
+ return (scr_context_is_main() && scr_sig_watched > 0) || scr_stdin_pending();
}
/* One dispatch pass at a loop turn: wake-pipe bytes are consumed (safe
* any time), flagged signals fire, and stdin is probed/served. */
static void scr_events_dispatch(void) {
- scr_wake_pipe_drain();
- scr_signals_drain();
+ if (scr_context_is_main()) {
+ scr_wake_pipe_drain();
+ scr_signals_drain();
+ }
if (scr_exc_pending()) return;
scr_stdin_service();
}
@@ -1188,8 +1201,8 @@ static void scr_events_dispatch(void) {
* any signal is watched, fd 0 while stdin has a consumer. */
static int scr_events_pollfds(int out[2]) {
int n = 0;
- if (scr_sig_watched > 0 && scr_wake_pipe[0] >= 0) out[n++] = scr_wake_pipe[0];
- if (scr_stdin_pending()) out[n++] = 0;
+ if (scr_context_is_main() && scr_sig_watched > 0 && scr_wake_pipe[0] >= 0) out[n++] = scr_wake_pipe[0];
+ if (scr_context_is_main() && scr_stdin_pending()) out[n++] = 0;
return n;
}
@@ -1206,7 +1219,7 @@ static void scr_exit_atexit(void) { scr_run_exit_listeners((double)scr_exit_code
* listeners fire first, then everything releases, then the RC audit sees
* a clean heap. */
static void scr_events_cleanup_atexit(void) {
- scr_sig_cleanup();
+ if (scr_context_is_main()) scr_sig_cleanup();
scr_stdin_settle_done();
scr_stdin_drop_listeners();
scr_bytes_release(scr_stdin_buffer);
@@ -1215,15 +1228,16 @@ static void scr_events_cleanup_atexit(void) {
}
void scr_events_install(void) {
- static bool installed = false;
+ static SCR_TL bool installed = false;
if (installed) return;
installed = true;
+ if (!scr_context_is_main()) scr_stdin_read_eof = true;
#ifdef __wasi__
int stdin_flags = fcntl(0, F_GETFL, 0);
if (stdin_flags >= 0) (void)fcntl(0, F_SETFL, stdin_flags | O_NONBLOCK);
#endif
- atexit(scr_events_cleanup_atexit);
- atexit(scr_exit_atexit);
+ scr_atexit(scr_events_cleanup_atexit);
+ scr_atexit(scr_exit_atexit);
scr_loop_set_events(&scr_events_pending, &scr_events_watching, &scr_events_dispatch,
&scr_events_pollfds);
scr_process_exit_hook = &scr_run_exit_listeners;
diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c
index 068a3f7f7..87d1259a4 100644
--- a/packages/runtime/src/scr_events_emitter.c
+++ b/packages/runtime/src/scr_events_emitter.c
@@ -97,6 +97,11 @@ struct ScrEeReg {
* only removeAllListeners deletes: the named form drops the one key,
* the no-argument wipe resets everything and leaves shape mode. */
bool shape;
+ /* Owner-internal observation of empty/nonempty listener transitions. It
+ * runs before user meta events and must never invoke script or mutate the
+ * emitter. The owner keeps the borrowed context alive with the emitter. */
+ void (*observe)(void *context, const ScrStr *name, bool present);
+ void *observer_context;
};
/* The vtable of BARE `new EventEmitter()` instances; the emitted main()
@@ -111,7 +116,7 @@ static SCR_TL bool scr_ee_trace_hint_shown = false;
/* Post-registration hook (scr_runtime.h): the stream unit's flow kick.
* NULL in stream-free builds — behavior byte-identical. */
-void (*scr_emitter_on_hook)(ScrEmitter *em, ScrStr *name) = NULL;
+SCR_TL void (*scr_emitter_on_hook)(ScrEmitter *em, ScrStr *name) = NULL;
/* ── entries and buckets ──────────────────────────────────────────────── */
@@ -137,6 +142,18 @@ static ScrEeReg *scr_ee_reg_ensure(ScrEmitter *em) {
return em->reg;
}
+void scr_emitter_observe(ScrEmitter *em,
+ void (*observe)(void *, const ScrStr *, bool), void *context) {
+ ScrEeReg *reg = scr_ee_reg_ensure(em);
+ reg->observe = observe;
+ reg->observer_context = context;
+}
+
+static void scr_ee_observe(ScrEmitter *em, const ScrStr *name, bool present) {
+ ScrEeReg *reg = em->reg;
+ if (reg->observe) reg->observe(reg->observer_context, name, present);
+}
+
static ScrEeBucket *scr_ee_bucket_find(ScrEeReg *reg, const char *name, size_t len) {
if (!reg) return NULL;
for (ScrEeBucket *b = reg->head; b; b = b->next) {
@@ -368,6 +385,7 @@ static ScrEmitter *scr_ee_add(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*mov
b->ls[b->n] = e;
}
b->n++;
+ if (b->n == 1) scr_ee_observe(em, name, true);
scr_ee_warn_maybe(em, b);
if (scr_emitter_on_hook != NULL) scr_emitter_on_hook(em, name);
return scr_emitter_retain(em);
@@ -461,6 +479,7 @@ static void scr_ee_remove_at(ScrEmitter *em, ScrEeBucket *b, size_t i) {
memmove(b->ls + i, b->ls + i + 1, (b->n - i - 1) * sizeof *b->ls);
b->n--;
ScrStr *name = scr_str_retain(b->name);
+ if (b->n == 0) scr_ee_observe(em, name, false);
if (b->n == 0 && !em->reg->shape) scr_ee_bucket_drop(em->reg, b);
scr_ee_entry_unref(e);
scr_ee_emit_meta(em, "removeListener", name);
@@ -519,6 +538,7 @@ static void scr_ee_remove_all_named(ScrEmitter *em, ScrEeBucket *b, bool meta) {
if (!meta) {
for (size_t i = 0; i < b->n; i++) scr_ee_entry_unref(b->ls[i]);
b->n = 0;
+ scr_ee_observe(em, b->name, false);
scr_ee_bucket_drop(em->reg, b);
return;
}
@@ -695,6 +715,19 @@ bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, si
return scr_ee_emit_core(em, name, NULL, args, argc);
}
+/* Handle event registries use checked callbacks for every tuple, including
+ * errors. Keep this separate from the statically typed Error ABI above. */
+bool scr_emitter_emit_error_flex(ScrEmitter *em, ScrDyn *error) {
+ if (!scr_ee_has(em->reg, "error")) {
+ scr_dyn_throw(scr_dyn_retain(error));
+ return false;
+ }
+ ScrStr *name = scr_str_new("error", 5);
+ bool had = scr_ee_emit_core(em, name, NULL, &error, 1);
+ scr_str_release(name);
+ return had;
+}
+
/* emit('error', err) — Node's special event: with no 'error' listener the
* payload THROWS (catchable; uncaught exits 1 with the error report).
* err is borrowed; the throw takes its own +1. */
diff --git a/packages/runtime/src/scr_exception.c b/packages/runtime/src/scr_exception.c
index 80972dada..b7a27a246 100644
--- a/packages/runtime/src/scr_exception.c
+++ b/packages/runtime/src/scr_exception.c
@@ -37,11 +37,11 @@ _Noreturn void scr_trap_fmt(const char *fmt, ...) {
#endif /* !SCR_LIB */
static SCR_TL ScrExcCell scr_main_exc; /* fiber zero (the main stack) */
-#if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
+#if (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS)
/* A thread-local pointer cannot be initialized with &scr_main_exc — the
* address of a thread-local is not a constant expression — so NULL stands
- * for the main cell and every read resolves it. Fibers never exist in
- * library artifacts, so the cell only ever IS the main cell here. */
+ * for the main cell and every read resolves it. Worker executables also
+ * switch this pointer as fibers enter and leave their own exception cells. */
static SCR_TL ScrExcCell *scr_cur;
#define SCR_EXC_CUR() (scr_cur ? scr_cur : &scr_main_exc)
#else
@@ -117,7 +117,12 @@ ScrStr *scr_stack_capture(void) {
#define scr_exc_release_fn (SCR_EXC_CUR()->release_fn)
#define scr_exc_trace_fn (SCR_EXC_CUR()->trace_fn)
-bool scr_exc_pending(void) { return scr_exc_kind != SCR_EXC_NONE; }
+bool scr_exc_pending(void) {
+#ifdef SCR_WORKERS
+ if (scr_context_checkpoint()) return true;
+#endif
+ return scr_exc_kind != SCR_EXC_NONE;
+}
/* Release the current payload (if any) and reset to NONE. Shared by clear,
* the uncaught printer, and every throw (replace semantics). */
@@ -212,12 +217,13 @@ void scr_rethrow(const ScrCaught *c) {
break;
case SCR_EXC_NONE:
break; /* unreachable: take() never stores NONE */
+ case SCR_EXC_TERMINATE:
case SCR_EXC_GENRET:
/* The generator-return sentinel, stashed across an exception-path
* finally (emitTryCatch): re-raising restores the bare kind — no
* payload exists. */
scr_exc_reset();
- scr_exc_kind = SCR_EXC_GENRET;
+ scr_exc_kind = c->kind;
break;
}
}
@@ -279,6 +285,7 @@ ScrStr *scr_caught_to_string(const ScrCaught *c) {
* boxes print this too where Node would vary (SEMANTICS.md). */
return scr_str_new("[object Object]", 15);
case SCR_EXC_NONE:
+ case SCR_EXC_TERMINATE:
case SCR_EXC_GENRET: /* unreachable: take() never stores NONE/the sentinel */
break;
}
@@ -428,10 +435,13 @@ static SCR_TL int scr_exit_code_hint = 0;
void scr_exit_code_note(int code) { scr_exit_code_hint = code; }
int scr_exit_code_hint_get(void) { return scr_exit_code_hint; }
-int (*scr_uncaught_exception_hook)(bool from_promise) = NULL;
-static bool scr_uncaught_handler_failed = false;
+SCR_TL int (*scr_uncaught_exception_hook)(bool from_promise) = NULL;
+static SCR_TL bool scr_uncaught_handler_failed = false;
bool scr_exc_handle_uncaught(bool from_promise) {
+#ifdef SCR_WORKERS
+ if (scr_context_stopping()) return false;
+#endif
if (!scr_exc_pending()) return true;
if (!scr_uncaught_exception_hook || scr_uncaught_handler_failed) return false;
int result = scr_uncaught_exception_hook(from_promise);
@@ -440,6 +450,14 @@ bool scr_exc_handle_uncaught(bool from_promise) {
}
void scr_exc_print_uncaught(void) {
+#ifdef SCR_WORKERS
+ if (scr_context_stopping()) { scr_exc_clear(); return; }
+ if (scr_context_report_error) {
+ scr_exit_code_note(scr_uncaught_handler_failed ? 7 : 1);
+ scr_context_report_error();
+ return;
+ }
+#endif
scr_exit_code_note(scr_uncaught_handler_failed ? 7 : 1);
/* Settle any runtime-internal stdout fragment before the stderr line when
* both share an fd. JavaScript-visible writes already flush themselves. */
@@ -477,6 +495,7 @@ void scr_exc_print_uncaught(void) {
break;
case SCR_EXC_NONE: /* main only calls this when pending */
case SCR_EXC_GENRET: /* unreachable: the trampoline consumes the sentinel */
+ case SCR_EXC_TERMINATE:
fputs("(no exception)", stderr);
break;
}
diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c
index a8ac4a8c0..849cdbfe6 100644
--- a/packages/runtime/src/scr_fetch.c
+++ b/packages/runtime/src/scr_fetch.c
@@ -1618,7 +1618,7 @@ static void sf_collector_append(SfCollector *c, const ScrBytes *bytes) {
c->data = next;
c->cap = cap;
}
- memcpy(c->data + c->len, bytes->data, bytes->len);
+ scr_bytes_read(bytes, 0, c->data + c->len, bytes->len);
c->len = need;
}
@@ -3536,8 +3536,8 @@ static char *sf_all_proxy;
static char *sf_no_proxy;
static char *sf_env_copy(const char *lower, const char *upper) {
- const char *value = getenv(lower);
- if (!value || value[0] == '\0') value = getenv(upper);
+ const char *value = scr_getenv(lower);
+ if (!value || value[0] == '\0') value = scr_getenv(upper);
if (!value || value[0] == '\0') return NULL;
size_t len = strlen(value);
char *copy = malloc(len + 1);
@@ -3547,7 +3547,7 @@ static char *sf_env_copy(const char *lower, const char *upper) {
}
static void sf_proxy_snapshot(void) {
- const char *optin = getenv("NODE_USE_ENV_PROXY");
+ const char *optin = scr_getenv("NODE_USE_ENV_PROXY");
sf_proxy_enabled = optin && strcmp(optin, "1") == 0;
if (!sf_proxy_enabled) return;
sf_http_proxy = sf_env_copy("http_proxy", "HTTP_PROXY");
@@ -4597,6 +4597,7 @@ static void sf_on_data(ScrClosure *cb, ScrBytes *chunk) {
return;
}
+ SCR_BYTES_SNAPSHOT(chunk);
sf_inflate_append(t, chunk->data, chunk->len);
sf_inflate_process(t);
sf_release(t);
@@ -5045,7 +5046,9 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
!(body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_WEB_STREAM)) {
if (body->kind == SCR_DYN_BYTES) {
- body_bytes = scr_bytes_from_data(body->v.bytes->data, body->v.bytes->len * scr_bytes_elem_size(body->v.bytes->elem));
+ ScrBytes *raw = scr_bytes_raw_view(body->v.bytes);
+ body_bytes = scr_bytes_copy(raw);
+ scr_bytes_release(raw);
} else {
ScrStr *text =
body->kind == SCR_DYN_STR
@@ -5397,7 +5400,7 @@ static ScrDyn *sf_data_response(ScrStr *url) {
}
bytes->len = n;
if (base64) {
- ScrStr *encoded = scr_str_new((const char *)bytes->data, bytes->len);
+ ScrStr *encoded = scr_bytes_string(bytes, 0, bytes->len);
ScrStr *encoding = scr_str_new("base64", 6);
ScrBytes *decoded = scr_bytes_from_str(encoded, encoding);
scr_str_release(encoded); scr_str_release(encoding); scr_bytes_release(bytes); bytes = decoded;
@@ -5768,7 +5771,7 @@ static void sf_teardown(void) {
}
void scr_fetch_install(void) {
- static bool installed;
+ static SCR_TL bool installed;
if (installed) return;
installed = true;
scr_tls_ca_install();
@@ -5808,7 +5811,7 @@ static void sf_signal_teardown(void) {
while (sf_reason_signals) sf_signal_drop_reason(sf_reason_signals);
}
static void sf_signal_install(void) {
- static bool installed;
+ static SCR_TL bool installed;
if (installed) return;
installed = true;
scr_dyn_handle_install(SCR_DYNH_ABORT_SIGNAL, &sf_signal_ops);
@@ -6001,9 +6004,9 @@ typedef struct FxTransfer {
struct FxTransfer *next;
} FxTransfer;
-static FxTransfer *fx_live = NULL; /* registry: +1 each */
-static size_t fx_nlive = 0;
-static int fx_next_id = 1;
+static SCR_TL FxTransfer *fx_live = NULL; /* registry: +1 each */
+static SCR_TL size_t fx_nlive = 0;
+static SCR_TL int fx_next_id = 1;
static FxTransfer *fx_retain(FxTransfer *t) {
t->rc++;
@@ -6305,15 +6308,15 @@ static bool fx_bad_port(int port) {
/* ── env proxy (NODE_USE_ENV_PROXY=1, undici's EnvHttpProxyAgent) ────── */
-static bool fx_proxy_enabled;
-static char *fx_http_proxy;
-static char *fx_https_proxy;
-static char *fx_all_proxy;
-static char *fx_no_proxy;
+static SCR_TL bool fx_proxy_enabled;
+static SCR_TL char *fx_http_proxy;
+static SCR_TL char *fx_https_proxy;
+static SCR_TL char *fx_all_proxy;
+static SCR_TL char *fx_no_proxy;
static char *fx_env_copy(const char *lower, const char *upper) {
- const char *v = getenv(lower);
- if (v == NULL || v[0] == '\0') v = getenv(upper);
+ const char *v = scr_getenv(lower);
+ if (v == NULL || v[0] == '\0') v = scr_getenv(upper);
if (v == NULL || v[0] == '\0') return NULL;
size_t len = strlen(v);
char *copy = malloc(len + 1);
@@ -6323,7 +6326,7 @@ static char *fx_env_copy(const char *lower, const char *upper) {
}
static void fx_proxy_snapshot(void) {
- const char *optin = getenv("NODE_USE_ENV_PROXY");
+ const char *optin = scr_getenv("NODE_USE_ENV_PROXY");
fx_proxy_enabled = optin != NULL && strcmp(optin, "1") == 0;
/* Snapshot even without Node's global opt-in: Vercel's dispatcher is a
* request-local proxy activation over the same startup environment. */
@@ -7500,7 +7503,7 @@ static void fx_teardown(void) {
* on real sockets the loop's own poller sleeps on, and armed island
* timers cap that sleep through the loop's island-deadline hook. */
void scr_fetch_install(void) {
- static bool installed = false;
+ static SCR_TL bool installed = false;
if (installed) return;
installed = true;
scr_tls_ca_install();
diff --git a/packages/runtime/src/scr_fetch_curl.c b/packages/runtime/src/scr_fetch_curl.c
index 650bc101d..a87922f4c 100644
--- a/packages/runtime/src/scr_fetch_curl.c
+++ b/packages/runtime/src/scr_fetch_curl.c
@@ -366,7 +366,7 @@ static JSValue fx_host_start(JSContext *ctx, JSValueConst this_val, int argc,
JSValue envproxyv = JS_GetPropertyStr(ctx, req, "useEnvProxy");
bool use_env_proxy = JS_ToBool(ctx, envproxyv) > 0;
JS_FreeValue(ctx, envproxyv);
- const char *global_env_proxy = getenv("NODE_USE_ENV_PROXY");
+ const char *global_env_proxy = scr_getenv("NODE_USE_ENV_PROXY");
use_env_proxy = use_env_proxy ||
(global_env_proxy != NULL &&
strcmp(global_env_proxy, "1") == 0);
diff --git a/packages/runtime/src/scr_ffi.c b/packages/runtime/src/scr_ffi.c
index 870a5f5ed..c42461228 100644
--- a/packages/runtime/src/scr_ffi.c
+++ b/packages/runtime/src/scr_ffi.c
@@ -2,12 +2,12 @@
#include
-/* The process-global retained-registration ledger. Format-4 script-thread
+/* The context-local retained-registration ledger. Format-4 script-thread
* tables use the lock-free path in this always-linked unit. Format-5 foreign
* tables install an optional teardown hook owned by scr_ffi_queue.c, keeping
* all queue/thread machinery out of unrelated binaries. */
-static ScrFfiTable *scr_ffi_tables;
-static bool scr_ffi_exit_registered;
+static SCR_TL ScrFfiTable *scr_ffi_tables;
+static SCR_TL bool scr_ffi_exit_registered;
static void scr_ffi_oom(void) { scr_trap("scriptc: out of memory\n"); }
diff --git a/packages/runtime/src/scr_file_handle.c b/packages/runtime/src/scr_file_handle.c
index 0777a8b22..da6d27265 100644
--- a/packages/runtime/src/scr_file_handle.c
+++ b/packages/runtime/src/scr_file_handle.c
@@ -385,6 +385,7 @@ void scr_file_handle_write_file(ScrFileHandle *h, ScrStr *data) {
}
void scr_file_handle_write_file_bytes(ScrFileHandle *h, ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
scr_file_handle_write_all(h, data->data,
data->len * scr_bytes_elem_size(data->elem));
}
diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c
index 90a74335d..5a9d45299 100644
--- a/packages/runtime/src/scr_http.c
+++ b/packages/runtime/src/scr_http.c
@@ -115,7 +115,7 @@ static const char *scr_http_reason(int code) {
return "unknown";
}
-static ScrDyn *scr_http_status_codes_value;
+static SCR_TL ScrDyn *scr_http_status_codes_value;
static void scr_http_status_codes_cleanup(void) {
scr_dyn_release(scr_http_status_codes_value);
@@ -135,7 +135,7 @@ ScrDyn *scr_http_status_codes(void) {
scr_str_release(value);
}
scr_http_status_codes_value = codes;
- atexit(scr_http_status_codes_cleanup);
+ scr_atexit(scr_http_status_codes_cleanup);
}
return scr_dyn_retain(scr_http_status_codes_value);
}
@@ -148,7 +148,7 @@ static const char *const scr_http_methods_table[] = {
"SOURCE", "SUBSCRIBE", "TRACE", "UNBIND", "UNLINK", "UNLOCK", "UNSUBSCRIBE",
};
-static ScrArr *scr_http_methods_value;
+static SCR_TL ScrArr *scr_http_methods_value;
static void scr_http_methods_cleanup(void) {
scr_arr_release(scr_http_methods_value);
@@ -164,7 +164,7 @@ ScrArr *scr_http_methods(void) {
scr_arr_push_ref(methods, scr_str_new(name, strlen(name)));
}
scr_http_methods_value = methods;
- atexit(scr_http_methods_cleanup);
+ scr_atexit(scr_http_methods_cleanup);
}
return scr_arr_retain(scr_http_methods_value);
}
@@ -179,12 +179,12 @@ ScrArr *scr_http_methods(void) {
* a request-registration hook so server.on("request", ...) on an
* h2-tagged server ctx routes to the h2 request list. The vtable's
* typedef lives in scr_runtime.h (both units name it). */
-static const ScrHttpH2Ops *scr_http_h2_ops = NULL;
-static void (*scr_http_h2_request_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
+static SCR_TL const ScrHttpH2Ops *scr_http_h2_ops = NULL;
+static SCR_TL void (*scr_http_h2_request_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *fn, bool once) = NULL;
-static void (*scr_http_h2_connect_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
+static SCR_TL void (*scr_http_h2_connect_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *h1_fn, void *h2_fn, bool once) = NULL;
-static void (*scr_http_h2_upgrade_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
+static SCR_TL void (*scr_http_h2_upgrade_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *fn, bool once) = NULL;
void scr_http_set_h2_ops(const ScrHttpH2Ops *ops) { scr_http_h2_ops = ops; }
@@ -247,7 +247,7 @@ struct ScrHttpReq {
};
#ifdef SCR_RC_AUDIT
-static long scr_http_live = 0;
+static SCR_TL long scr_http_live = 0;
long scr_http_live_count(void) { return scr_http_live; }
#endif
@@ -1510,6 +1510,7 @@ void scr_http_res_write_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
}
void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
double expected;
if (r->head_sent && scr_http_res_expected_length(r, &expected) &&
(double)(r->strict_bytes_written + (data->len * scr_bytes_elem_size(data->elem))) > expected) {
@@ -1642,6 +1643,7 @@ void scr_http_res_end_str(ScrHttpRes *r, ScrStr *data /*borrowed*/) {
}
void scr_http_res_end_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
scr_http_res_end_raw(r, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
@@ -1790,8 +1792,8 @@ typedef struct {
void *h; /* +1 */
} ScrHttpEmit;
-static ScrHttpEmit *scr_http_emits = NULL;
-static size_t scr_http_emits_head = 0, scr_http_emits_len = 0, scr_http_emits_cap = 0;
+static SCR_TL ScrHttpEmit *scr_http_emits = NULL;
+static SCR_TL size_t scr_http_emits_head = 0, scr_http_emits_len = 0, scr_http_emits_cap = 0;
static void scr_http_client_release_internal(struct ScrHttpClientReq *c);
static void scr_http_client_settle(struct ScrHttpClientReq *c);
@@ -1804,7 +1806,7 @@ static void scr_http_client_emit_abort(struct ScrHttpClientReq *c);
* has drained for the last time, teardown paths that would queue close
* emits (scr_net_cleanup_atexit freeing in-flight parsers) discard the
* payload instead — nothing will ever sweep again. */
-static bool scr_http_exiting = false;
+static SCR_TL bool scr_http_exiting = false;
static void scr_http_emit_release(ScrHttpEmit *e);
@@ -2007,10 +2009,10 @@ static void scr_http_cleanup_atexit(void) {
/* One-time wiring into the net sweep (both createServer and request call
* through here). */
static void scr_http_install(void) {
- static bool installed = false;
+ static SCR_TL bool installed = false;
if (installed) return;
installed = true;
- atexit(scr_http_cleanup_atexit);
+ scr_atexit(scr_http_cleanup_atexit);
scr_net_set_proto_sweep(&scr_http_proto_pending, &scr_http_proto_sweep);
}
@@ -3186,7 +3188,7 @@ struct ScrHttpClientReq {
struct ScrHttpClientReq *next;
};
-static ScrHttpClientReq *scr_http_clients = NULL; /* registry: +1 each */
+static SCR_TL ScrHttpClientReq *scr_http_clients = NULL; /* registry: +1 each */
ScrHttpClientReq *scr_http_client_retain(ScrHttpClientReq *c) {
if (c->rc != SIZE_MAX) c->rc++;
@@ -3552,6 +3554,7 @@ void scr_http_client_write_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
}
void scr_http_client_write_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
scr_http_client_write_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
@@ -3608,6 +3611,7 @@ void scr_http_client_end_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/) {
}
void scr_http_client_end_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
scr_http_client_end_raw(c, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
@@ -4277,7 +4281,7 @@ typedef struct ScrHttpAgent {
struct ScrHttpAgent *next;
} ScrHttpAgent;
-static ScrHttpAgent *scr_http_agents = NULL; /* registry: +1 each, atexit-swept */
+static SCR_TL ScrHttpAgent *scr_http_agents = NULL; /* registry: +1 each, atexit-swept */
static ScrHttpAgent *scr_http_agent_retain(ScrHttpAgent *a) {
a->rc++;
diff --git a/packages/runtime/src/scr_http2.c b/packages/runtime/src/scr_http2.c
index 3b2fdfd36..0087fea4e 100644
--- a/packages/runtime/src/scr_http2.c
+++ b/packages/runtime/src/scr_http2.c
@@ -215,7 +215,7 @@ typedef struct ScrH2HuffNode {
int sym; /* -1 = interior */
} ScrH2HuffNode;
-static ScrH2HuffNode *scr_h2_huff_root = NULL;
+static SCR_TL ScrH2HuffNode *scr_h2_huff_root = NULL;
static ScrH2HuffNode *scr_h2_huff_node(void) {
ScrH2HuffNode *n = calloc(1, sizeof *n);
@@ -762,8 +762,8 @@ struct ScrH2Session {
/* ── refcounts + the RC audit ────────────────────────────────────────── */
#ifdef SCR_RC_AUDIT
-static long scr_h2_sessions_live = 0;
-static long scr_h2_streams_live = 0;
+static SCR_TL long scr_h2_sessions_live = 0;
+static SCR_TL long scr_h2_streams_live = 0;
long scr_http2_live_count(void) { return scr_h2_sessions_live + scr_h2_streams_live; }
#endif
@@ -885,9 +885,9 @@ typedef struct {
void *h; /* +1, typed by kind */
} ScrH2Emit;
-static ScrH2Emit *scr_h2_emits = NULL;
-static size_t scr_h2_emits_head = 0, scr_h2_emits_len = 0, scr_h2_emits_cap = 0;
-static bool scr_h2_exiting = false;
+static SCR_TL ScrH2Emit *scr_h2_emits = NULL;
+static SCR_TL size_t scr_h2_emits_head = 0, scr_h2_emits_len = 0, scr_h2_emits_cap = 0;
+static SCR_TL bool scr_h2_exiting = false;
static void scr_h2_emit_release(ScrH2Emit *e) {
switch (e->kind) {
@@ -1249,10 +1249,10 @@ static void scr_h2_connect_hook(void *ctx, ScrClosure *cb, void *h1_fn, void *h2
static void scr_h2_upgrade_hook(void *ctx, ScrClosure *cb, void *fn, bool once);
static void scr_http2_install(void) {
- static bool installed = false;
+ static SCR_TL bool installed = false;
if (installed) return;
installed = true;
- atexit(scr_h2_cleanup_atexit);
+ scr_atexit(scr_h2_cleanup_atexit);
scr_net_set_proto_sweep(&scr_h2_proto_pending, &scr_h2_proto_sweep);
/* the compat seam: scr_http.c routes h2-backed responses through the
* ops vtable and h2-tagged 'request' registrations through the hook */
@@ -1887,6 +1887,7 @@ void scr_http2_stream_write_str(ScrH2Stream *st, ScrStr *data /*borrowed*/) {
}
void scr_http2_stream_write_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
scr_h2_stream_send(st, (const char *)data->data, (size_t)(data->len * scr_bytes_elem_size(data->elem)));
}
@@ -1904,6 +1905,7 @@ void scr_http2_stream_end_str(ScrH2Stream *st, ScrStr *data /*borrowed*/) {
}
void scr_http2_stream_end_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
if (st->destroyed || st->want_end || scr_h2_stream_local_closed(st)) return;
scr_h2_buf_put(&st->wbuf, (const char *)data->data, (size_t)(data->len * scr_bytes_elem_size(data->elem)));
st->want_end = true;
diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c
index ff20cb425..70cf9cf86 100644
--- a/packages/runtime/src/scr_inspect.c
+++ b/packages/runtime/src/scr_inspect.c
@@ -629,6 +629,7 @@ ScrStr *scr_insp_regex(ScrRegex *re) {
* runs before the depth check in Node). u8 element kind only (the
* frontend fences the rest). */
ScrStr *scr_insp_buffer(ScrBytes *b) {
+ SCR_SHARED_GUARD(b, NULL);
InspBuf out = {0};
ib_cstr(&out, "len < 50 ? b->len : 50;
diff --git a/packages/runtime/src/scr_island.c b/packages/runtime/src/scr_island.c
index 189898835..c73408b41 100644
--- a/packages/runtime/src/scr_island.c
+++ b/packages/runtime/src/scr_island.c
@@ -587,7 +587,7 @@ static void isl_init(void) {
isl_register_hostfn_class();
/* LIFO: registered after scr_init's handlers, so teardown runs before
* the cycle collection + RC audit — the audit sees the engine gone. */
- atexit(isl_teardown_at_exit);
+ scr_atexit(isl_teardown_at_exit);
}
/* Nesting depth of host-function callbacks (scriptc closures invoked BY
diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c
index 369be5d92..563b5d822 100644
--- a/packages/runtime/src/scr_json.c
+++ b/packages/runtime/src/scr_json.c
@@ -2268,7 +2268,9 @@ bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe) {
if (!value) return false;
if (value->kind == SCR_DYN_JSVAL) return scr_dyn_jsval_ops()->type_probe(value->v.jsval.cell, probe);
#define PROBE(name) (probe->len == sizeof(name) - 1 && memcmp(probe->data, name, sizeof(name) - 1) == 0)
- if (PROBE("isAnyArrayBuffer") || PROBE("isArrayBuffer")) return scr_array_buffer_is(value);
+ if (PROBE("isAnyArrayBuffer")) return scr_buffer_storage_is(value);
+ if (PROBE("isArrayBuffer")) return scr_array_buffer_is(value);
+ if (PROBE("isSharedArrayBuffer")) return scr_shared_array_buffer_is(value);
if (PROBE("isArrayBufferView")) return value->kind == SCR_DYN_BYTES;
if (PROBE("isDataView")) return value->kind == SCR_DYN_BYTES && value->v.bytes->is_data_view;
if (PROBE("isTypedArray")) return value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view;
@@ -2506,7 +2508,7 @@ void scr_dyn_chunk_enc(bool utf8) { scr_dyn_chunk_utf8 = utf8; }
* a Buffer-flavored bytes box, or a string inside a setEncoding window. */
ScrDyn *scr_dyn_new_chunk(const ScrBytes *b) {
if (scr_dyn_chunk_utf8) {
- ScrStr *s = scr_str_new((const char *)b->data, b->len);
+ ScrStr *s = scr_bytes_string(b, 0, b->len);
ScrDyn *d = scr_dyn_new_str(s);
scr_str_release(s);
return d;
@@ -2684,7 +2686,7 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) {
* always-linked core never references gated units. A missing tag at use
* is an internal error: emitted programs install a unit's ops whenever
* they can box its handles. */
-static const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_COUNT];
+static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_COUNT];
void scr_dyn_handle_install(ScrDynHandleTag tag, const ScrDynHandleOps *ops) {
scr_dynh_ops[tag] = ops;
@@ -2931,6 +2933,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) {
d->v.handle.ptr = scr_dyn_handle_ops(tag)->retain(h);
d->v.handle.tag = tag;
d->v.handle.traced = tag == SCR_DYNH_WEAK_MAP || tag == SCR_DYNH_WEAK_SET ||
+ tag == SCR_DYNH_WORKER || tag == SCR_DYNH_MESSAGE_PORT ||
((tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) &&
(((ScrMap *)h)->key_trace != NULL || ((ScrMap *)h)->val_trace != NULL));
return d;
@@ -2940,8 +2943,8 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) {
* allocator view; the freelist stays private, and the release arm's
* scr_promise_release edge installs HERE — a promise-free link never
* references the fiber machinery (the unit-test subset links). */
-void (*scr_dyn_promise_release_fn)(ScrPromise *p) = NULL;
-bool (*scr_dyn_promise_identity_fn)(ScrPromise *a, ScrPromise *b) = NULL;
+SCR_TL void (*scr_dyn_promise_release_fn)(ScrPromise *p) = NULL;
+SCR_TL bool (*scr_dyn_promise_identity_fn)(ScrPromise *a, ScrPromise *b) = NULL;
ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p)) {
scr_dyn_promise_release_fn = release_fn;
@@ -3106,7 +3109,15 @@ typedef struct {
static SCR_TL ScrDynThisEnt *scr_dyn_this_stack;
static SCR_TL size_t scr_dyn_this_n, scr_dyn_this_cap;
+static void scr_dyn_this_cleanup(void) {
+ while (scr_dyn_this_n) scr_dyn_this_pop();
+ free(scr_dyn_this_stack);
+ scr_dyn_this_stack = NULL;
+ scr_dyn_this_cap = 0;
+}
+
static ScrDynThisEnt *scr_dyn_this_grow(void) {
+ if (!scr_dyn_this_stack) scr_atexit(scr_dyn_this_cleanup);
if (scr_dyn_this_n == scr_dyn_this_cap) {
size_t cap = scr_dyn_this_cap ? scr_dyn_this_cap * 2 : 8;
ScrDynThisEnt *s = realloc(scr_dyn_this_stack, cap * sizeof *s);
@@ -4315,7 +4326,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
case SCR_DYN_OBJ: tag = "[object Object]"; break;
case SCR_DYN_HANDLE:
if ((d->v.handle.tag >= SCR_DYNH_ABORT_SIGNAL &&
- d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER ||
+ d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER ||
d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET ||
d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP ||
d->v.handle.tag == SCR_DYNH_DATE) {
@@ -4808,7 +4819,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
return out;
}
if ((d->v.handle.tag >= SCR_DYNH_ABORT_SIGNAL &&
- d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP) {
+ d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "[object ");
@@ -8735,6 +8746,14 @@ bool scr_array_buffer_is(const ScrDyn *value) {
return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_ARRAY_BUFFER;
}
+bool scr_shared_array_buffer_is(const ScrDyn *value) {
+ return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER;
+}
+
+bool scr_buffer_storage_is(const ScrDyn *value) {
+ return scr_array_buffer_is(value) || scr_shared_array_buffer_is(value);
+}
+
bool scr_array_buffer_is_view(const ScrDyn *value) {
return value->kind == SCR_DYN_BYTES;
}
@@ -8743,8 +8762,9 @@ static ScrDyn *scr_array_buffer_get(void *h, const char *key, size_t len) {
if ((len == 10 && memcmp(key, "byteLength", len) == 0) ||
(len == 13 && memcmp(key, "maxByteLength", len) == 0))
return scr_dyn_new_num(scr_bytes_byte_len(h));
- if ((len == 9 && memcmp(key, "resizable", len) == 0) ||
- (len == 8 && memcmp(key, "detached", len) == 0)) return scr_dyn_new_bool(false);
+ if (len == 8 && !memcmp(key, "growable", len) && ((ScrBytes *)h)->shared) return scr_dyn_new_bool(false);
+ if (!((ScrBytes *)h)->shared && ((len == 9 && memcmp(key, "resizable", len) == 0) ||
+ (len == 8 && memcmp(key, "detached", len) == 0))) return scr_dyn_new_bool(false);
return NULL;
}
@@ -8763,6 +8783,7 @@ static ScrDyn *scr_array_buffer_invoke(void *h, ScrDyn *self, const char *method
ScrBytes *view = scr_bytes_buffer_view(h, SCR_BYTES_U8, 0, false, 0);
if (!view) return NULL;
ScrBytes *copy = scr_bytes_slice(view, start, end);
+ if (((ScrBytes *)h)->shared) scr_bytes_make_shared(copy);
scr_bytes_release(view);
ScrDyn *result = scr_array_buffer_from_bytes(copy);
scr_bytes_release(copy);
@@ -8778,8 +8799,13 @@ ScrDyn *scr_array_buffer_from_bytes(ScrBytes *view) {
"ArrayBuffer", &scr_bytes_retain_v, &scr_bytes_release_v,
&scr_array_buffer_invoke, &scr_array_buffer_get, &scr_array_buffer_set, NULL,
};
- scr_dyn_handle_install(SCR_DYNH_ARRAY_BUFFER, &ops);
- return scr_dyn_new_handle(view->backing ? view->backing : view, SCR_DYNH_ARRAY_BUFFER);
+ static const ScrDynHandleOps shared_ops = {
+ "SharedArrayBuffer", &scr_bytes_retain_v, &scr_bytes_release_v,
+ &scr_array_buffer_invoke, &scr_array_buffer_get, &scr_array_buffer_set, NULL,
+ };
+ ScrDynHandleTag tag = view->shared ? SCR_DYNH_SHARED_ARRAY_BUFFER : SCR_DYNH_ARRAY_BUFFER;
+ scr_dyn_handle_install(tag, view->shared ? &shared_ops : &ops);
+ return scr_dyn_new_handle(view->backing ? view->backing : view, tag);
}
static ScrDyn *scr_ffi_fail(const char *message) {
@@ -8810,7 +8836,8 @@ ScrDyn *scr_ffi_argument(ScrDyn *value, ScrStr *type) {
void *address = NULL;
bool converted = value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF;
ScrBytes *bytes = value->kind == SCR_DYN_BYTES ? value->v.bytes :
- scr_array_buffer_is(value) ? value->v.handle.ptr : NULL;
+ scr_buffer_storage_is(value) ? value->v.handle.ptr : NULL;
+ if (bytes && bytes->shared) return scr_ffi_fail("Shared byte storage cannot expose an unmanaged native pointer");
if (bytes) { address = bytes->data; converted = true; }
if (value->kind == SCR_DYN_STR) {
if (memchr(value->v.str->data, 0, value->v.str->len)) {
@@ -9060,6 +9087,7 @@ static ScrDyn *scr_ffi_memory_call(ScrClosure *closure, ScrDyn *const *args, siz
scr_dyn_arg_type_fail("source", "an ArrayBuffer or ArrayBufferView", value);
return NULL;
}
+ if (bytes->shared) return scr_ffi_fail("Shared byte storage cannot expose an unmanaged native pointer");
ScrBigInt *pointer = scr_bigint_from_pointer(bytes->data);
ScrDyn *result = scr_dyn_new_bigint(pointer);
scr_bigint_release(pointer);
@@ -9247,7 +9275,7 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) {
*ptr = key->v.fn.class_obj ? (void *)key->v.fn.class_obj : (void *)key->v.fn.clo;
*kind = key->v.fn.class_obj ? 3 : 2; return 1;
case SCR_DYN_HANDLE:
- if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP &&
+ if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_SHARED_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP &&
key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO &&
key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE &&
key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET) return -1;
@@ -9478,6 +9506,16 @@ ScrDyn *scr_array_buffer_new(ScrDyn *length) {
return result;
}
+ScrDyn *scr_shared_array_buffer_new(ScrDyn *length) {
+ ScrDyn *buffer = scr_array_buffer_new(length);
+ if (!buffer) return NULL;
+ ScrBytes *storage = buffer->v.handle.ptr;
+ scr_bytes_make_shared(storage);
+ ScrDyn *result = scr_array_buffer_from_bytes(storage);
+ scr_dyn_release(buffer);
+ return result;
+}
+
double scr_array_buffer_byte_length_getter(void) {
ScrDyn *receiver = scr_dyn_this_get();
if (!scr_array_buffer_is(receiver)) {
@@ -9504,7 +9542,7 @@ ScrDyn *scr_array_buffer_byte_length_descriptor(ScrDyn *getter) {
ScrBytes *scr_array_buffer_view(ScrBytesElem elem, const ScrDyn *buffer,
const ScrDyn *offset, const ScrDyn *length) {
- if (!scr_array_buffer_is(buffer)) {
+ if (!scr_buffer_storage_is(buffer)) {
// Non-buffer inputs use the length/iterable/array-like constructor.
// Their extra arguments are evaluated by the caller, but not coerced.
return scr_bytes_from_dyn(elem, buffer, false);
@@ -9539,7 +9577,7 @@ SCR_ARRAY_BUFFER_VIEW(f64, SCR_BYTES_F64)
#undef SCR_ARRAY_BUFFER_VIEW
ScrBytes *scr_array_buffer_view_dv(ScrDyn *buffer, ScrDyn *offset, ScrDyn *length) {
- if (!scr_array_buffer_is(buffer)) {
+ if (!scr_buffer_storage_is(buffer)) {
static const char msg[] = "First argument to DataView constructor must be an ArrayBuffer";
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
return NULL;
diff --git a/packages/runtime/src/scr_json.test.ts b/packages/runtime/src/scr_json.test.ts
index af140f0d9..5ba23b397 100644
--- a/packages/runtime/src/scr_json.test.ts
+++ b/packages/runtime/src/scr_json.test.ts
@@ -41,6 +41,7 @@ test.each([false, true])(
"scr_cycle.c",
"scr_lib.c",
"scr_bytes.c",
+ "scr_shared.c",
"scr_bigint.c",
"scr_url.c",
"scr_url_params.c",
diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c
index 80bf49374..66176d6b8 100644
--- a/packages/runtime/src/scr_lib.c
+++ b/packages/runtime/src/scr_lib.c
@@ -17,6 +17,11 @@
#include "scr_runtime.h"
#include
+#ifdef SCR_WORKERS
+#include
+static atomic_uint scr_process_mask;
+static void scr_process_mask_init(void);
+#endif
#include
#include
#include
@@ -253,6 +258,10 @@ static bool scr_lib_same_executable_arg(const char *a, const char *b) {
* Load the documented OS parser from System32 without adding an executable-
* only shell dependency to the embedding runtime's link contract. */
static void scr_lib_prepare_utf8_argv(void) {
+#ifdef SCR_WORKERS
+ /* Worker arguments were copied as UTF-8 before their native thread began. */
+ if (!scr_context_is_main()) return;
+#endif
if (scr_lib_argv == NULL || scr_lib_argv_utf8 != NULL) return;
HMODULE shell = LoadLibraryExW(L"shell32.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (shell == NULL) scr_trap("could not load Windows command-line parser");
@@ -325,6 +334,9 @@ bool scr_lib_should_collapse_reexec_arg(ScrStr *cmd, ScrArr *args) {
* the archive's objects free of any atexit reference — the K8 ambient
* audit's bar). */
void scr_lib_init(int argc, char **argv) {
+#ifdef SCR_WORKERS
+ if (scr_context_is_main()) scr_process_mask_init();
+#endif
scr_lib_argc = argc;
scr_lib_argv = argv;
if (argc >= 2 && strncmp(argv[1], "--scriptc-fork=", 15) == 0) {
@@ -345,7 +357,7 @@ void scr_lib_init(int argc, char **argv) {
scr_lib_argv = scr_lib_argv_owned;
}
}
- atexit(scr_lib_cleanup);
+ scr_atexit(scr_lib_cleanup);
}
#endif /* !SCR_LIB */
@@ -564,7 +576,7 @@ ScrStr *scr_process_platform(void) {
scr_platform_str = scr_str_new("unknown", 7);
#endif
#ifndef SCR_LIB
- atexit(scr_process_platform_cleanup);
+ scr_atexit(scr_process_platform_cleanup);
#endif
}
return scr_str_retain(scr_platform_str);
@@ -586,7 +598,7 @@ ScrStr *scr_process_arch(void) {
scr_arch_str = scr_str_new("unknown", 7);
#endif
#ifndef SCR_LIB
- atexit(scr_process_arch_cleanup);
+ scr_atexit(scr_process_arch_cleanup);
#endif
}
return scr_str_retain(scr_arch_str);
@@ -602,7 +614,7 @@ ScrStr *scr_process_versions_node(void) {
scr_versions_node_str =
scr_str_new(SCR_NODE_COMPAT_VERSION, sizeof(SCR_NODE_COMPAT_VERSION) - 1);
#ifndef SCR_LIB
- atexit(scr_process_versions_node_cleanup);
+ scr_atexit(scr_process_versions_node_cleanup);
#endif
}
return scr_str_retain(scr_versions_node_str);
@@ -621,7 +633,7 @@ ScrStr *scr_process_versions_openssl(void) {
scr_versions_openssl_str =
scr_str_new(SCR_OPENSSL_COMPAT_VERSION, sizeof(SCR_OPENSSL_COMPAT_VERSION) - 1);
#ifndef SCR_LIB
- atexit(scr_process_versions_openssl_cleanup);
+ scr_atexit(scr_process_versions_openssl_cleanup);
#endif
}
return scr_str_retain(scr_versions_openssl_str);
@@ -684,13 +696,19 @@ ScrStr *scr_process_exec_path(void) {
#endif
scr_exec_path_str = scr_str_new(use, strlen(use));
#ifndef SCR_LIB
- atexit(scr_process_exec_path_cleanup);
+ scr_atexit(scr_process_exec_path_cleanup);
#endif
}
return scr_str_retain(scr_exec_path_str);
}
ScrStr *scr_env_get(const ScrStr *name) {
+#ifdef SCR_WORKERS
+ if (!scr_context_is_main()) {
+ const char *value = scr_context_getenv(name->data);
+ return value ? scr_str_new(value, strlen(value)) : NULL;
+ }
+#endif
/* ScrStr data is NUL-terminated (like the fs paths below). A fresh copy
* per read: getenv's buffer is not ours to alias, and Node's process.env
* reads snapshot the value too. Absent → NULL (the compiler's undefined
@@ -710,7 +728,7 @@ ScrStr *scr_env_get(const ScrStr *name) {
free(buf);
return s;
#else
- const char *v = getenv(name->data);
+ const char *v = scr_getenv(name->data);
return v ? scr_str_new(v, strlen(v)) : NULL;
#endif
}
@@ -719,6 +737,9 @@ ScrStr *scr_env_get(const ScrStr *name) {
* children (posix_spawn inherits environ) observe the write, like Node.
* Both args borrowed (NUL-terminated ScrStr data); setenv copies. */
void scr_env_set(const ScrStr *name, const ScrStr *value) {
+#ifdef SCR_WORKERS
+ if (scr_context_env_set(name->data, value->data)) return;
+#endif
#ifdef _WIN32
/* The WIN32 environment (see scr_env_get): an empty value stays a
* present-but-empty variable, and children inherit the write. */
@@ -731,6 +752,9 @@ void scr_env_set(const ScrStr *name, const ScrStr *value) {
/* `delete process.env.NAME` — unsetenv(3): later reads answer absent and
* spawned children lose the variable, like Node. Borrowed. */
void scr_env_unset(const ScrStr *name) {
+#ifdef SCR_WORKERS
+ if (scr_context_env_set(name->data, NULL)) return;
+#endif
#ifdef _WIN32
SetEnvironmentVariableA(name->data, NULL);
#else
@@ -886,6 +910,7 @@ void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const ch
scr_jb_puts(&b, "' must be a string, Uint8Array, or URL without null bytes. Received ");
if (value->kind == SCR_DYN_BYTES) {
const ScrBytes *bytes = value->v.bytes;
+ SCR_SHARED_GUARD(bytes, NULL);
if (value->buffer) {
scr_jb_puts(&b, "len; i++) {
@@ -936,7 +961,7 @@ void scr_process_load_env_file(const ScrDyn *value) {
if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) path = scr_str_new("./.env", 6);
else if (value->kind == SCR_DYN_STR) path = scr_str_retain(value->v.str);
else if (scr_dyn_native_url_is(value)) path = scr_url_to_path(value->v.handle.ptr);
- else if (scr_dyn_bytes_is(value, SCR_BYTES_U8)) path = scr_str_new((const char *)value->v.bytes->data, value->v.bytes->len);
+ else if (scr_dyn_bytes_is(value, SCR_BYTES_U8)) path = scr_bytes_string(value->v.bytes, 0, value->v.bytes->len);
else {
scr_dyn_arg_type_fail("path", "of type string or an instance of Buffer or URL", value);
return;
@@ -978,6 +1003,10 @@ void scr_process_load_env_file(const ScrDyn *value) {
* (not producible by setenv) are skipped; the value is everything after
* the FIRST '='. +1 array. */
ScrArr *scr_env_pairs(void) {
+#ifdef SCR_WORKERS
+ ScrArr *local = scr_context_env_pairs();
+ if (local) return local;
+#endif
ScrArr *out = scr_arr_new(SCR_ELEM_STR, 0);
#ifdef _WIN32
/* The WIN32 environment block (see scr_env_get): NUL-separated
@@ -1297,7 +1326,7 @@ bool scr_proc_stream_write(double fd, const ScrStr *data) {
* directory API. The env var covers every real session; abort matches
* the POSIX arm's stance on the unreachable failure. */
ScrStr *scr_os_homedir(void) {
- const char *home = getenv("USERPROFILE");
+ const char *home = scr_getenv("USERPROFILE");
if (home && home[0] != '\0') return scr_str_new(home, strlen(home));
scr_trap("scriptc: os.homedir() failed\n");
}
@@ -1371,11 +1400,11 @@ ScrStr *scr_os_tmpdir(void) {
* physical-memory query. Keep the useful environment-backed answers and
* spell the platform explicitly for the rest. */
ScrStr *scr_os_homedir(void) {
- const char *home = getenv("HOME");
+ const char *home = scr_getenv("HOME");
return scr_str_new(home ? home : "", home ? strlen(home) : 0);
}
ScrStr *scr_os_user_name(void) {
- const char *user = getenv("USER");
+ const char *user = scr_getenv("USER");
return scr_str_new(user ? user : "", user ? strlen(user) : 0);
}
ScrStr *scr_os_user_shell(void) { return scr_str_new("", 0); }
@@ -1391,7 +1420,7 @@ ScrStr *scr_os_tmpdir(void) { return scr_str_new("/tmp", 4); }
ScrStr *scr_os_homedir(void) {
/* uv_os_homedir: $HOME when set (even empty is "set" only if non-NULL;
* libuv requires non-empty), else the passwd entry. */
- const char *home = getenv("HOME");
+ const char *home = scr_getenv("HOME");
if (home && home[0] != '\0') return scr_str_new(home, strlen(home));
struct passwd pw;
struct passwd *result = NULL;
@@ -1468,9 +1497,9 @@ double scr_os_totalmem(void) {
ScrStr *scr_os_tmpdir(void) {
/* Node's env cascade, with ONE trailing slash trimmed (never down to
* nothing: "/" stays "/"). */
- const char *dir = getenv("TMPDIR");
- if (!dir || dir[0] == '\0') dir = getenv("TMP");
- if (!dir || dir[0] == '\0') dir = getenv("TEMP");
+ const char *dir = scr_getenv("TMPDIR");
+ if (!dir || dir[0] == '\0') dir = scr_getenv("TMP");
+ if (!dir || dir[0] == '\0') dir = scr_getenv("TEMP");
if (!dir || dir[0] == '\0') dir = "/tmp";
size_t len = strlen(dir);
if (len > 1 && dir[len - 1] == '/') len--;
@@ -1743,8 +1772,8 @@ void scr_os_ifaddrs_free(ScrIfaddrs *s) {
/* The events-unit hooks (scr_events.c fills them at install; NULL in
* event-free binaries and in the standalone runtime C tests). */
-void (*scr_process_exit_hook)(double code) = NULL;
-void (*scr_stdin_destroy_hook)(void) = NULL;
+SCR_TL void (*scr_process_exit_hook)(double code) = NULL;
+SCR_TL void (*scr_stdin_destroy_hook)(void) = NULL;
void scr_process_set_exit_code(double code) {
if (!isfinite(code) || trunc(code) != code) {
@@ -1782,6 +1811,13 @@ void scr_process_exit(double code) {
* non-NULL only when the events unit is linked (scr_events.c). */
scr_process_in_exit = true;
if (scr_process_exit_hook != NULL) scr_process_exit_hook(code);
+#ifdef SCR_WORKERS
+ if (!scr_context_is_main()) {
+ /* A nested exit or a failing exit listener already selected its code. */
+ if (!scr_context_stopping()) scr_context_stop((int)code);
+ return;
+ }
+#endif
/* _Exit skips atexit handlers on purpose: no further code runs (matching
* Node), and the RC audit is meaningless mid-program (live values are
* expected). scr_init's flush-at-exit is also skipped — flush here. */
@@ -1796,7 +1832,7 @@ void scr_process_exit(double code) {
* attribute monotonic stamp — the binary's own start, which is what
* "the current Node.js process" means for a compiled program). */
#ifdef _WIN32
-#if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
+#if (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS)
/* The uptime anchor belongs to the host PROCESS, not to a library
* instance. InitOnce keeps the lazy Windows spelling race-free when
* several thread instances ask for the clock concurrently. */
@@ -1840,7 +1876,7 @@ __attribute__((constructor)) static void scr_uptime_anchor_init(void) {
#endif
double scr_process_uptime(void) {
-#if defined(_WIN32) && defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
+#if defined(_WIN32) && ((defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS))
scr_uptime_anchor_init();
#elif defined(_WIN32)
if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init();
@@ -1852,7 +1888,7 @@ double scr_process_uptime(void) {
* start (Node's timeOrigin anchor), fractional — the same monotonic
* clock and anchor as uptime, in Node's performance.now units. */
double scr_perf_now(void) {
-#if defined(_WIN32) && defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
+#if defined(_WIN32) && ((defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS))
scr_uptime_anchor_init();
#elif defined(_WIN32)
if (scr_uptime_t0_ms == 0) scr_uptime_anchor_init();
@@ -2092,9 +2128,34 @@ SCR_TL bool scr_process_in_exit = false;
bool scr_process_exiting(void) { return scr_process_in_exit; }
+#ifdef SCR_WORKERS
+/* Initialize before workers start. Reading through the cached process value
+ * avoids temporarily clearing the OS mask while another thread creates a file. */
+static void scr_process_mask_init(void) {
+#ifdef _WIN32
+ int current, ignored;
+ _umask_s(0, ¤t);
+ _umask_s(current, &ignored);
+#else
+ unsigned current = umask(0);
+ umask((mode_t)current);
+#endif
+ atomic_store_explicit(&scr_process_mask, (unsigned)current, memory_order_relaxed);
+}
+#endif
+
/* umask(2): mask < 0 reads without setting (set 0, restore — umask has no
* read-only form); otherwise sets and answers the previous mask. */
double scr_process_umask(double mask) {
+#ifdef SCR_WORKERS
+ if (mask < 0) return atomic_load_explicit(&scr_process_mask, memory_order_relaxed);
+ if (!scr_context_is_main()) {
+ static const char message[] = "Setting process.umask() is not supported in workers";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "ERR_WORKER_UNSUPPORTED_OPERATION");
+ return 0;
+ }
+ atomic_store_explicit(&scr_process_mask, (unsigned)mask, memory_order_relaxed);
+#endif
#ifdef _WIN32
/* Node on Windows accepts umask() calls; only the low bits matter. */
int prev;
@@ -2124,6 +2185,13 @@ double scr_process_umask(double mask) {
}
void scr_process_chdir(ScrStr *dir) {
+#ifdef SCR_WORKERS
+ if (!scr_context_is_main()) {
+ static const char message[] = "process.chdir() is not supported in workers";
+ scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_WORKER_UNSUPPORTED_OPERATION");
+ return;
+ }
+#endif
#ifdef _WIN32
WCHAR *wide = scr_fs_win_wide(dir);
BOOL changed = wide && SetCurrentDirectoryW(wide);
@@ -3141,6 +3209,7 @@ static double scr_fs_write_bytes(double fd, const void *data, size_t length,
* Node/libuv. Positioned writes do not advance the descriptor. */
double scr_fs_write_sync(double fd, ScrBytes *buf, double offset, double length,
double position) {
+ SCR_BYTES_SNAPSHOT(buf);
size_t bytelen = buf->len; /* frontend admits u8 buffers only */
char msg[160];
char recv[48];
@@ -3271,9 +3340,18 @@ double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length,
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE");
return 0;
}
+ uint8_t *bounce = buf->shared ? malloc(want ? want : 1) : NULL;
+ if (buf->shared && !bounce) scr_trap("scriptc: out of memory\n");
+ uint8_t *destination = bounce ? bounce : buf->data + off;
ssize_t n = position == -1
- ? read((int)fd, buf->data + off, want)
- : scr_fs_pread((int)fd, buf->data + off, want, position);
+ ? read((int)fd, destination, want)
+ : scr_fs_pread((int)fd, destination, want, position);
+ int read_error = errno;
+ if (bounce) {
+ if (n > 0) scr_bytes_write(buf, off, bounce, (size_t)n);
+ free(bounce);
+ }
+ errno = read_error;
if (n < 0) {
int e = errno;
char namebuf[16];
@@ -3319,7 +3397,7 @@ static double scr_fs_vector_sync(double fd, ScrArr *buffers, double position,
size_t at = 0;
for (size_t i = 0; i < count; i++) {
ScrBytes *buf = scr_arr_get_ref(buffers, (double)i);
- if (buf->len) memcpy(data + at, buf->data, buf->len);
+ scr_bytes_read(buf, 0, data + at, buf->len);
at += buf->len;
scr_bytes_release(buf);
}
@@ -3336,7 +3414,7 @@ static double scr_fs_vector_sync(double fd, ScrArr *buffers, double position,
for (size_t i = 0; i < count && remaining; i++) {
ScrBytes *buf = scr_arr_get_ref(buffers, (double)i);
size_t n = buf->len < remaining ? buf->len : remaining;
- if (n) memcpy(buf->data, data + at, n);
+ scr_bytes_write(buf, 0, data + at, n);
at += n;
remaining -= n;
scr_bytes_release(buf);
@@ -3911,43 +3989,6 @@ ScrBytes *scr_fs_read_fd_bytes(double fd) {
return scr_bytes_take_data(out.bytes, out.len);
}
-/* ── Atomics.wait: the synchronous-sleep idiom ───────────────────────
- * Atomics.wait(int32Array, idx, expected, timeoutMs). scriptc has no
- * threads: nothing can ever notify a waiter, so the spec's behavior for
- * every compilable program is exactly "compare, then sleep out the
- * timeout" — "not-equal" immediately when the element differs from
- * `expected`, "timed-out" after a real nanosleep otherwise ("ok" is
- * unreachable; the compiler requires the timeout argument, since an
- * infinite wait here would be a certain deadlock). The sleep resumes
- * across EINTR so signals don't shorten it. Timeout semantics follow the
- * spec: NaN/+Infinity would be infinite (compiler-fenced by requiring
- * the argument, but a runtime NaN clamps to 0 defensively), negatives
- * clamp to 0. */
-ScrStr *scr_atomics_wait(ScrBytes *arr, double idx, double expected, double timeout_ms) {
- double have = scr_bytes_get(arr, idx); /* traps out-of-range like every access */
- /* The comparison is on the stored int32 vs ToInt32(expected). */
- double t = expected;
- if (t != t || isinf(t)) t = 0;
- else {
- t = trunc(t);
- t = fmod(t, 4294967296.0);
- if (t < 0) t += 4294967296.0;
- if (t >= 2147483648.0) t -= 4294967296.0;
- }
- if (have != t) return scr_str_new("not-equal", 9);
- double ms = timeout_ms;
- if (ms != ms || ms < 0) ms = 0;
- if (ms > 0) {
- struct timespec left = {
- (time_t)(ms / 1000.0),
- (long)((ms - (double)(time_t)(ms / 1000.0) * 1000.0) * 1e6),
- };
- struct timespec rem;
- while (nanosleep(&left, &rem) != 0 && errno == EINTR) left = rem;
- }
- return scr_str_new("timed-out", 9);
-}
-
/* ── the tty probes ──────────────────────────────────────────────────── */
bool scr_process_is_tty(double fd) { return isatty((int)fd) != 0; }
@@ -4467,7 +4508,7 @@ static DWORD scr_fs_win_junction(const WCHAR *target, const WCHAR *destination)
#ifdef _WIN32
/* libuv keeps this fallback for later calls once Windows rejects the flag. */
-static DWORD scr_fs_symlink_usermode_flag = 0x2; /* ALLOW_UNPRIVILEGED_CREATE */
+static SCR_TL DWORD scr_fs_symlink_usermode_flag = 0x2; /* ALLOW_UNPRIVILEGED_CREATE */
#endif
int scr_fs_symlink_infer(ScrStr *absolute) {
@@ -5738,6 +5779,7 @@ ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key) {
}
ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key) {
+ SCR_BYTES_SNAPSHOT(key);
ScrDigestAlg kind;
if (!scr_digest_alg(alg->data, alg->len, &kind)) {
scr_crypto_invalid_digest(alg);
@@ -5778,6 +5820,7 @@ ScrCryptoHash *scr_crypto_hash_update_str(ScrCryptoHash *hash, ScrStr *data) {
}
ScrCryptoHash *scr_crypto_hash_update_bytes(ScrCryptoHash *hash, ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
return scr_crypto_hash_update_raw(hash, data->data,
data->len * scr_bytes_elem_size(data->elem));
}
@@ -5849,10 +5892,12 @@ ScrStr *scr_crypto_hash_digest_str(ScrStr *alg, ScrStr *data, ScrStr *enc) {
}
ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc) {
+ SCR_BYTES_SNAPSHOT(data);
return scr_hash_digest_raw(alg, data->data, data->len * scr_bytes_elem_size(data->elem), enc);
}
bool scr_crypto_timing_safe_equal(ScrBytes *left, ScrBytes *right) {
+ SCR_SHARED_GUARD(left, right);
size_t left_len = left->len * scr_bytes_elem_size(left->elem);
size_t right_len = right->len * scr_bytes_elem_size(right->elem);
if (left_len != right_len) {
@@ -5897,7 +5942,13 @@ ScrBytes *scr_crypto_random_fill(ScrBytes *bytes, double offset, double size) {
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)n, "ERR_OUT_OF_RANGE");
return NULL;
}
- arc4random_buf(bytes->data + (size_t)offset, (size_t)size);
+ if (bytes->shared) {
+ uint8_t *random = malloc(size ? (size_t)size : 1);
+ if (!random) scr_trap("scriptc: out of memory\n");
+ arc4random_buf(random, (size_t)size);
+ scr_bytes_write(bytes, (size_t)offset, random, (size_t)size);
+ free(random);
+ } else arc4random_buf(bytes->data + (size_t)offset, (size_t)size);
return scr_bytes_retain(bytes);
}
@@ -5991,6 +6042,8 @@ static ScrBytes *scr_crypto_pbkdf2_raw(const unsigned char *password, size_t pas
ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt,
double iterations, double keylen, ScrStr *digest) {
+ SCR_BYTES_SNAPSHOT(password);
+ SCR_BYTES_SNAPSHOT(salt);
if (!isfinite(iterations) || floor(iterations) != iterations ||
iterations < 1 || iterations > 2147483647.0) {
char value[64], msg[176];
@@ -6051,6 +6104,9 @@ static bool scr_crypto_integer(double value, const char *name, double max) {
ScrBytes *scr_crypto_hkdf_bytes(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt,
ScrBytes *info, double keylen) {
+ SCR_BYTES_SNAPSHOT(ikm);
+ SCR_BYTES_SNAPSHOT(salt);
+ SCR_BYTES_SNAPSHOT(info);
if (!scr_crypto_integer(keylen, "length", 9007199254740991.0)) return NULL;
size_t info_len = info->len * scr_bytes_elem_size(info->elem);
if (info_len > 1024) {
@@ -6175,6 +6231,8 @@ static bool scr_scrypt_params(double n, double r, double p, double maxmem, size_
ScrBytes *scr_crypto_scrypt_derive(ScrBytes *password, ScrBytes *salt, double keylen,
double n, double r, double p, double maxmem) {
+ SCR_BYTES_SNAPSHOT(password);
+ SCR_BYTES_SNAPSHOT(salt);
if (!scr_crypto_integer(keylen, "keylen", 2147483647.0) ||
!scr_crypto_integer(n, "N", 4294967295.0) || !scr_crypto_integer(r, "r", 4294967295.0) ||
!scr_crypto_integer(p, "p", 4294967295.0) || !scr_crypto_integer(maxmem, "maxmem", 9007199254740991.0)) return NULL;
@@ -6305,6 +6363,7 @@ ScrBytes *scr_crypto_scrypt(ScrBytes *password, ScrBytes *salt, double keylen, S
static ScrStr *scr_x509_fingerprint_raw(const uint8_t *in, size_t n);
ScrStr *scr_crypto_x509_fingerprint(ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
return scr_x509_fingerprint_raw(data->data, data->len * scr_bytes_elem_size(data->elem));
}
@@ -6531,6 +6590,7 @@ static ScrStr *scr_x509_validity_raw(const uint8_t *in, size_t n, bool want_to)
}
ScrStr *scr_crypto_x509_valid_from(ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
return scr_x509_validity_raw(data->data, data->len * scr_bytes_elem_size(data->elem), false);
}
@@ -6539,6 +6599,7 @@ ScrStr *scr_crypto_x509_valid_from_str(ScrStr *pem) {
}
ScrStr *scr_crypto_x509_valid_to(ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
return scr_x509_validity_raw(data->data, data->len * scr_bytes_elem_size(data->elem), true);
}
diff --git a/packages/runtime/src/scr_mailbox.c b/packages/runtime/src/scr_mailbox.c
new file mode 100644
index 000000000..0bc9abf04
--- /dev/null
+++ b/packages/runtime/src/scr_mailbox.c
@@ -0,0 +1,233 @@
+#ifndef _DARWIN_C_SOURCE
+#define _DARWIN_C_SOURCE 1
+#endif
+#ifndef _POSIX_C_SOURCE
+#define _POSIX_C_SOURCE 200809L
+#endif
+#include "scr_mailbox.h"
+
+#include
+#include
+#include
+#include
+#ifdef _WIN32
+#include
+#else
+#include
+#include
+#include
+#include
+#endif
+
+struct ScrMailbox {
+ atomic_size_t references;
+ ScrMailEvent *head;
+ ScrMailEvent **tail;
+ bool closed;
+#ifdef _WIN32
+ SRWLOCK lock;
+ CONDITION_VARIABLE condition;
+#else
+ pthread_mutex_t lock;
+ pthread_cond_t condition;
+ int read_fd;
+ int write_fd;
+#endif
+};
+
+static void scr_mailbox_lock(ScrMailbox *mailbox) {
+#ifdef _WIN32
+ AcquireSRWLockExclusive(&mailbox->lock);
+#else
+ (void)pthread_mutex_lock(&mailbox->lock);
+#endif
+}
+
+static void scr_mailbox_unlock(ScrMailbox *mailbox) {
+#ifdef _WIN32
+ ReleaseSRWLockExclusive(&mailbox->lock);
+#else
+ (void)pthread_mutex_unlock(&mailbox->lock);
+#endif
+}
+
+static void scr_mailbox_signal(ScrMailbox *mailbox) {
+#ifdef _WIN32
+ WakeAllConditionVariable(&mailbox->condition);
+#else
+ unsigned char byte = 1;
+ /* Nonblocking wakeups coalesce when the pipe is full. The protected
+ * queue is authoritative, so a full pipe cannot lose a message. */
+ ssize_t written;
+ do { written = write(mailbox->write_fd, &byte, 1); } while (written < 0 && errno == EINTR);
+ (void)pthread_cond_broadcast(&mailbox->condition);
+#endif
+}
+
+static void scr_mailbox_drain(ScrMailbox *mailbox) {
+#ifndef _WIN32
+ unsigned char bytes[128];
+ ssize_t count;
+ do { count = read(mailbox->read_fd, bytes, sizeof bytes); }
+ while (count > 0 || (count < 0 && errno == EINTR));
+#else
+ (void)mailbox;
+#endif
+}
+
+ScrMailbox *scr_mailbox_new(void) {
+ ScrMailbox *mailbox = calloc(1, sizeof(*mailbox));
+ if (!mailbox) scr_trap("scriptc: out of memory\n");
+ atomic_init(&mailbox->references, 1);
+ mailbox->tail = &mailbox->head;
+#ifdef _WIN32
+ InitializeSRWLock(&mailbox->lock);
+ InitializeConditionVariable(&mailbox->condition);
+#else
+ int pipe_fd[2];
+ if (pipe(pipe_fd) != 0) goto failed;
+ mailbox->read_fd = pipe_fd[0];
+ mailbox->write_fd = pipe_fd[1];
+ if (fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK) < 0 ||
+ fcntl(pipe_fd[1], F_SETFL, O_NONBLOCK) < 0 ||
+ fcntl(pipe_fd[0], F_SETFD, FD_CLOEXEC) < 0 ||
+ fcntl(pipe_fd[1], F_SETFD, FD_CLOEXEC) < 0) goto failed_pipe;
+ if (pthread_mutex_init(&mailbox->lock, NULL) != 0) goto failed_pipe;
+ pthread_condattr_t attributes;
+ if (pthread_condattr_init(&attributes) != 0) goto failed_lock;
+#if !defined(__APPLE__)
+ if (pthread_condattr_setclock(&attributes, CLOCK_MONOTONIC) != 0) {
+ pthread_condattr_destroy(&attributes);
+ goto failed_lock;
+ }
+#endif
+ int error = pthread_cond_init(&mailbox->condition, &attributes);
+ pthread_condattr_destroy(&attributes);
+ if (error != 0) goto failed_lock;
+#endif
+ return mailbox;
+#ifndef _WIN32
+failed_lock:
+ pthread_mutex_destroy(&mailbox->lock);
+failed_pipe:
+ close(pipe_fd[0]);
+ close(pipe_fd[1]);
+failed:
+ free(mailbox);
+ static const char message[] = "Could not create worker message channel";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "ERR_WORKER_INIT_FAILED");
+ return NULL;
+#endif
+}
+
+ScrMailbox *scr_mailbox_retain(ScrMailbox *mailbox) {
+ atomic_fetch_add_explicit(&mailbox->references, 1, memory_order_relaxed);
+ return mailbox;
+}
+
+void scr_mail_event_free(ScrMailEvent *event) {
+ if (!event) return;
+ scr_message_free(event->message);
+ free(event);
+}
+
+void scr_mailbox_release(ScrMailbox *mailbox) {
+ if (!mailbox || atomic_fetch_sub_explicit(&mailbox->references, 1, memory_order_acq_rel) != 1) return;
+ while (mailbox->head) {
+ ScrMailEvent *event = mailbox->head;
+ mailbox->head = event->next;
+ scr_mail_event_free(event);
+ }
+#ifndef _WIN32
+ pthread_cond_destroy(&mailbox->condition);
+ pthread_mutex_destroy(&mailbox->lock);
+ close(mailbox->read_fd);
+ close(mailbox->write_fd);
+#endif
+ free(mailbox);
+}
+
+bool scr_mailbox_post(ScrMailbox *mailbox, ScrMailEvent *event) {
+ scr_mailbox_lock(mailbox);
+ if (mailbox->closed) {
+ scr_mailbox_unlock(mailbox);
+ scr_mail_event_free(event);
+ return false;
+ }
+ event->next = NULL;
+ *mailbox->tail = event;
+ mailbox->tail = &event->next;
+ scr_mailbox_signal(mailbox);
+ scr_mailbox_unlock(mailbox);
+ return true;
+}
+
+ScrMailEvent *scr_mailbox_take(ScrMailbox *mailbox) {
+ scr_mailbox_lock(mailbox);
+ ScrMailEvent *event = mailbox->head;
+ if (event) {
+ mailbox->head = event->next;
+ event->next = NULL;
+ }
+ if (!mailbox->head) {
+ mailbox->tail = &mailbox->head;
+ scr_mailbox_drain(mailbox);
+ }
+ scr_mailbox_unlock(mailbox);
+ return event;
+}
+
+void scr_mailbox_close(ScrMailbox *mailbox) {
+ scr_mailbox_lock(mailbox);
+ mailbox->closed = true;
+ scr_mailbox_signal(mailbox);
+ scr_mailbox_unlock(mailbox);
+}
+
+bool scr_mailbox_pending(ScrMailbox *mailbox) {
+ scr_mailbox_lock(mailbox);
+ bool pending = mailbox->head != NULL;
+ scr_mailbox_unlock(mailbox);
+ return pending;
+}
+
+void scr_mailbox_wait(ScrMailbox *mailbox, double milliseconds) {
+ if (!(milliseconds > 0)) return;
+ scr_mailbox_lock(mailbox);
+ if (!mailbox->head && !mailbox->closed) {
+#ifdef _WIN32
+ DWORD timeout = milliseconds >= INFINITE ? INFINITE : (DWORD)ceil(milliseconds);
+ (void)SleepConditionVariableSRW(&mailbox->condition, &mailbox->lock, timeout, 0);
+#else
+ if (isinf(milliseconds)) (void)pthread_cond_wait(&mailbox->condition, &mailbox->lock);
+ else {
+ /* The event loop rechecks its monotonic deadlines after every wake,
+ * including spurious ones. Bound conversion before casting time_t. */
+ if (milliseconds > 86400000) milliseconds = 86400000;
+ struct timespec timeout = {(time_t)(milliseconds / 1000),
+ (long)ceil(fmod(milliseconds, 1000) * 1000000)};
+ if (timeout.tv_nsec == 1000000000) { timeout.tv_sec++; timeout.tv_nsec = 0; }
+#ifdef __APPLE__
+ (void)pthread_cond_timedwait_relative_np(&mailbox->condition, &mailbox->lock, &timeout);
+#else
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ timeout.tv_sec += now.tv_sec;
+ timeout.tv_nsec += now.tv_nsec;
+ if (timeout.tv_nsec >= 1000000000) { timeout.tv_sec++; timeout.tv_nsec -= 1000000000; }
+ (void)pthread_cond_timedwait(&mailbox->condition, &mailbox->lock, &timeout);
+#endif
+ }
+#endif
+ }
+ scr_mailbox_unlock(mailbox);
+}
+
+int scr_mailbox_pollfd(const ScrMailbox *mailbox) {
+#ifdef _WIN32
+ (void)mailbox;
+ return -1;
+#else
+ return mailbox->read_fd;
+#endif
+}
diff --git a/packages/runtime/src/scr_mailbox.h b/packages/runtime/src/scr_mailbox.h
new file mode 100644
index 000000000..792660bdb
--- /dev/null
+++ b/packages/runtime/src/scr_mailbox.h
@@ -0,0 +1,28 @@
+#ifndef SCR_MAILBOX_H
+#define SCR_MAILBOX_H
+
+#include "scr_message.h"
+
+typedef struct ScrMailbox ScrMailbox;
+typedef struct ScrMailEvent {
+ unsigned kind;
+ uint64_t source;
+ int code;
+ ScrMessage *message;
+ struct ScrMailEvent *next;
+} ScrMailEvent;
+
+ScrMailbox *scr_mailbox_new(void);
+ScrMailbox *scr_mailbox_retain(ScrMailbox *mailbox);
+void scr_mailbox_release(ScrMailbox *mailbox);
+/* Posting always consumes the event, including when the destination is
+ * closed. Taking returns an owned event; no runtime object crosses a queue. */
+bool scr_mailbox_post(ScrMailbox *mailbox, ScrMailEvent *event);
+ScrMailEvent *scr_mailbox_take(ScrMailbox *mailbox);
+void scr_mail_event_free(ScrMailEvent *event);
+void scr_mailbox_close(ScrMailbox *mailbox);
+bool scr_mailbox_pending(ScrMailbox *mailbox);
+void scr_mailbox_wait(ScrMailbox *mailbox, double milliseconds);
+int scr_mailbox_pollfd(const ScrMailbox *mailbox);
+
+#endif
diff --git a/packages/runtime/src/scr_mailbox.test.c b/packages/runtime/src/scr_mailbox.test.c
new file mode 100644
index 000000000..950cf77a8
--- /dev/null
+++ b/packages/runtime/src/scr_mailbox.test.c
@@ -0,0 +1,86 @@
+#undef NDEBUG
+#include "scr_mailbox.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+enum { PRODUCERS = 4, EVENTS = 4096 };
+typedef struct {
+ ScrMailbox *mailbox;
+ unsigned source;
+} Producer;
+static atomic_uint waiting;
+static atomic_bool closed;
+
+static ScrMailEvent *make_event(unsigned source, int code) {
+ ScrMailEvent *event = calloc(1, sizeof(*event));
+ assert(event);
+ event->source = source;
+ event->code = code;
+ return event;
+}
+
+static void *produce(void *data) {
+ Producer *producer = data;
+ for (int i = 0; i < EVENTS; i++)
+ assert(scr_mailbox_post(producer->mailbox, make_event(producer->source, i)));
+ scr_mailbox_release(producer->mailbox);
+ return NULL;
+}
+
+static void *post_after_close(void *data) {
+ ScrMailbox *mailbox = data;
+ atomic_fetch_add_explicit(&waiting, 1, memory_order_release);
+ while (!atomic_load_explicit(&closed, memory_order_acquire)) sched_yield();
+ scr_mailbox_wait(mailbox, INFINITY);
+ assert(!scr_mailbox_post(mailbox, make_event(0, 1)));
+ scr_mailbox_release(mailbox);
+ return NULL;
+}
+
+int main(void) {
+ scr_init();
+ ScrMailbox *mailbox = scr_mailbox_new();
+ assert(mailbox && !scr_exc_pending());
+ assert(!scr_mailbox_pending(mailbox));
+ pthread_t threads[PRODUCERS];
+ Producer producers[PRODUCERS];
+ for (unsigned i = 0; i < PRODUCERS; i++) {
+ producers[i] = (Producer){scr_mailbox_retain(mailbox), i};
+ assert(pthread_create(&threads[i], NULL, produce, &producers[i]) == 0);
+ }
+ for (unsigned i = 0; i < PRODUCERS; i++) assert(pthread_join(threads[i], NULL) == 0);
+
+ /* Filling before draining exercises coalesced wakeups and a full pipe.
+ * Per-producer order must survive arbitrary cross-producer interleaving. */
+ int next[PRODUCERS] = {0};
+ struct pollfd ready = {scr_mailbox_pollfd(mailbox), POLLIN, 0};
+ for (unsigned i = 0; i < PRODUCERS * EVENTS; i++) {
+ assert(scr_mailbox_pending(mailbox));
+ assert(poll(&ready, 1, 0) == 1 && (ready.revents & POLLIN));
+ ScrMailEvent *event = scr_mailbox_take(mailbox);
+ assert(event && event->source < PRODUCERS && event->next == NULL);
+ assert(event->code == next[event->source]++);
+ scr_mail_event_free(event);
+ }
+ assert(!scr_mailbox_pending(mailbox));
+ assert(!scr_mailbox_take(mailbox));
+ assert(poll(&ready, 1, 0) == 0);
+
+ /* Closing before a waiter starts sleeping must also wake it. Drop the
+ * creator's reference while producers retain the mailbox independently. */
+ for (unsigned i = 0; i < PRODUCERS; i++)
+ assert(pthread_create(&threads[i], NULL, post_after_close, scr_mailbox_retain(mailbox)) == 0);
+ while (atomic_load_explicit(&waiting, memory_order_acquire) != PRODUCERS) sched_yield();
+ scr_mailbox_close(mailbox);
+ scr_mailbox_release(mailbox);
+ atomic_store_explicit(&closed, true, memory_order_release);
+ for (unsigned i = 0; i < PRODUCERS; i++) assert(pthread_join(threads[i], NULL) == 0);
+ scr_context_cleanup();
+ puts("mailboxes preserve concurrent FIFO delivery, wakeups and close ownership");
+}
diff --git a/packages/runtime/src/scr_mailbox.test.ts b/packages/runtime/src/scr_mailbox.test.ts
new file mode 100644
index 000000000..80cb87d52
--- /dev/null
+++ b/packages/runtime/src/scr_mailbox.test.ts
@@ -0,0 +1,46 @@
+import { execFile } from "node:child_process";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { promisify } from "node:util";
+import { expect, test } from "vitest";
+import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js";
+
+const exec = promisify(execFile);
+
+test.skipIf(process.platform === "win32")(
+ "mailboxes coalesce concurrent wakeups and release closed destinations safely",
+ async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-mailbox-"));
+ try {
+ const bin = join(dir, "mailboxes");
+ await exec("clang", [
+ "-std=c11",
+ "-O1",
+ "-pthread",
+ "-DSCR_WORKERS",
+ "-DSCR_RC_AUDIT",
+ "-fsanitize=address,undefined",
+ "-Wno-deprecated-declarations",
+ ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
+ join(import.meta.dirname, "scr_mailbox.test.c"),
+ join(import.meta.dirname, "scr_mailbox.c"),
+ join(import.meta.dirname, "scr_message.c"),
+ ...EXECUTABLE_RUNTIME_SOURCES.map((name) => join(import.meta.dirname, name)),
+ ...(process.platform === "linux" ? ["-lm"] : []),
+ "-o",
+ bin,
+ ]);
+ const result = await exec(bin, [], {
+ timeout: 15000,
+ env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" },
+ });
+ expect(result.stdout).toBe(
+ "mailboxes preserve concurrent FIFO delivery, wakeups and close ownership\n",
+ );
+ expect(result.stderr).toBe("");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ },
+);
diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c
new file mode 100644
index 000000000..676469fb8
--- /dev/null
+++ b/packages/runtime/src/scr_message.c
@@ -0,0 +1,443 @@
+#include "scr_message.h"
+
+#include
+
+typedef enum {
+ SCR_MESSAGE_UNDEFINED,
+ SCR_MESSAGE_NULL,
+ SCR_MESSAGE_BOOL,
+ SCR_MESSAGE_NUMBER,
+ SCR_MESSAGE_STRING,
+ SCR_MESSAGE_BIGINT,
+ SCR_MESSAGE_ARRAY,
+ SCR_MESSAGE_OBJECT,
+ SCR_MESSAGE_STORAGE,
+ SCR_MESSAGE_BYTES,
+ SCR_MESSAGE_ARRAY_BUFFER,
+ SCR_MESSAGE_ERROR,
+} ScrMessageKind;
+
+typedef struct {
+ char *key;
+ size_t length;
+ size_t value;
+} ScrMessageProperty;
+
+typedef struct {
+ ScrMessageKind kind;
+ double number;
+ unsigned char *data;
+ ScrSharedBytes *shared;
+ size_t length;
+ ScrMessageProperty *properties;
+ size_t count;
+ size_t capacity;
+ size_t storage;
+ size_t offset;
+ ScrBytesElem element;
+ bool data_view;
+} ScrMessageNode;
+
+struct ScrMessage {
+ ScrMessageNode *nodes;
+ size_t count;
+ size_t capacity;
+};
+
+typedef struct {
+ const ScrDyn *input;
+ ScrDyn *view;
+ ScrDyn *keys;
+ size_t cursor;
+} ScrMessageVisit;
+
+typedef struct {
+ const void *identity;
+ unsigned domain;
+ size_t node;
+} ScrMessageIdentity;
+
+typedef struct {
+ ScrMessage *message;
+ ScrMessageVisit *visits;
+ ScrMessageIdentity *identities;
+ size_t identity_count;
+ size_t identity_capacity;
+ size_t *stack;
+ size_t depth;
+ size_t stack_capacity;
+} ScrMessageEncoder;
+
+static void *scr_message_alloc(size_t count, size_t size) {
+ if (count > SIZE_MAX / size) scr_trap("scriptc: message too large\n");
+ void *data = calloc(count ? count : 1, size);
+ if (!data) scr_trap("scriptc: out of memory\n");
+ return data;
+}
+
+static size_t scr_message_grow(size_t capacity) {
+ if (capacity > SIZE_MAX / 2) scr_trap("scriptc: message too large\n");
+ return capacity ? capacity * 2 : 16;
+}
+
+static unsigned char *scr_message_copy(const void *data, size_t length) {
+ if (length == SIZE_MAX) scr_trap("scriptc: message too large\n");
+ unsigned char *copy = scr_message_alloc(length + 1, 1);
+ if (length) memcpy(copy, data, length);
+ return copy;
+}
+
+static size_t scr_message_node(ScrMessageEncoder *encoder) {
+ ScrMessage *message = encoder->message;
+ if (message->count == message->capacity) {
+ size_t capacity = scr_message_grow(message->capacity);
+ ScrMessageNode *nodes = scr_message_alloc(capacity, sizeof(*nodes));
+ ScrMessageVisit *visits = scr_message_alloc(capacity, sizeof(*visits));
+ if (message->count) {
+ memcpy(nodes, message->nodes, message->count * sizeof(*nodes));
+ memcpy(visits, encoder->visits, message->count * sizeof(*visits));
+ }
+ free(message->nodes);
+ free(encoder->visits);
+ message->nodes = nodes;
+ encoder->visits = visits;
+ message->capacity = capacity;
+ }
+ return message->count++;
+}
+
+static size_t scr_message_hash(const void *identity, unsigned domain) {
+ uintptr_t value = (uintptr_t)identity;
+ value ^= value >> 16;
+ value *= (uintptr_t)0x45d9f3b;
+ value ^= value >> 16;
+ return (size_t)value ^ domain;
+}
+
+static size_t scr_message_find(const ScrMessageEncoder *encoder, const void *identity,
+ unsigned domain) {
+ if (!identity || !encoder->identity_capacity) return SIZE_MAX;
+ size_t slot = scr_message_hash(identity, domain) & (encoder->identity_capacity - 1);
+ while (encoder->identities[slot].identity) {
+ const ScrMessageIdentity *entry = &encoder->identities[slot];
+ if (entry->identity == identity && entry->domain == domain) return entry->node;
+ slot = (slot + 1) & (encoder->identity_capacity - 1);
+ }
+ return SIZE_MAX;
+}
+
+static void scr_message_identify(ScrMessageEncoder *encoder, const void *identity,
+ unsigned domain, size_t node) {
+ if (!identity) return;
+ if (encoder->identity_count >= encoder->identity_capacity / 2) {
+ size_t capacity = scr_message_grow(encoder->identity_capacity);
+ ScrMessageIdentity *entries = scr_message_alloc(capacity, sizeof(*entries));
+ for (size_t i = 0; i < encoder->identity_capacity; i++) {
+ ScrMessageIdentity entry = encoder->identities[i];
+ if (!entry.identity) continue;
+ size_t slot = scr_message_hash(entry.identity, entry.domain) & (capacity - 1);
+ while (entries[slot].identity) slot = (slot + 1) & (capacity - 1);
+ entries[slot] = entry;
+ }
+ free(encoder->identities);
+ encoder->identities = entries;
+ encoder->identity_capacity = capacity;
+ }
+ size_t slot = scr_message_hash(identity, domain) & (encoder->identity_capacity - 1);
+ while (encoder->identities[slot].identity) slot = (slot + 1) & (encoder->identity_capacity - 1);
+ encoder->identities[slot] = (ScrMessageIdentity){identity, domain, node};
+ encoder->identity_count++;
+}
+
+static void scr_message_push(ScrMessageEncoder *encoder, size_t node) {
+ if (encoder->depth == encoder->stack_capacity) {
+ size_t capacity = scr_message_grow(encoder->stack_capacity);
+ size_t *stack = scr_message_alloc(capacity, sizeof(*stack));
+ if (encoder->depth) memcpy(stack, encoder->stack, encoder->depth * sizeof(*stack));
+ free(encoder->stack);
+ encoder->stack = stack;
+ encoder->stack_capacity = capacity;
+ }
+ encoder->stack[encoder->depth++] = node;
+}
+
+static size_t scr_message_storage(ScrMessageEncoder *encoder, const ScrBytes *bytes) {
+ const ScrBytes *owner = bytes->backing ? bytes->backing : bytes;
+ size_t found = scr_message_find(encoder, owner, SCR_MESSAGE_STORAGE + 1);
+ if (found != SIZE_MAX) return found;
+ size_t node = scr_message_node(encoder);
+ scr_message_identify(encoder, owner, SCR_MESSAGE_STORAGE + 1, node);
+ ScrMessageNode *storage = &encoder->message->nodes[node];
+ storage->kind = SCR_MESSAGE_STORAGE;
+ size_t size = scr_bytes_elem_size(owner->elem);
+ if (owner->len > SIZE_MAX / size) scr_trap("scriptc: message too large\n");
+ storage->length = owner->len * size;
+ if (owner->shared) storage->shared = scr_shared_retain(owner->shared);
+ else storage->data = scr_message_copy(owner->data, storage->length);
+ return node;
+}
+
+static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) {
+ const void *identity = NULL;
+ unsigned domain = 0;
+ switch (input->kind) {
+ case SCR_DYN_TYPED_REF: identity = input->v.typed_ref.ptr; domain = 1; break;
+ case SCR_DYN_OBJ:
+ identity = input->v.obj.source_identity ? input->v.obj.source_identity : input;
+ domain = input->v.obj.source_identity ? 1 : 2;
+ break;
+ case SCR_DYN_ARR: identity = input; domain = 2; break;
+ case SCR_DYN_BYTES: identity = input->v.bytes; domain = 3; break;
+ case SCR_DYN_HANDLE: identity = input->v.handle.ptr; domain = 4; break;
+ default: break;
+ }
+ size_t found = scr_message_find(encoder, identity, domain);
+ if (found != SIZE_MAX) return found;
+ size_t id = scr_message_node(encoder);
+ scr_message_identify(encoder, identity, domain, id);
+ encoder->visits[id].input = scr_dyn_retain((ScrDyn *)input);
+ ScrDyn *value = input->kind == SCR_DYN_TYPED_REF
+ ? scr_dyn_typed_ref_materialize(input) : (ScrDyn *)input;
+ encoder->visits[id].view = value;
+ if (!value || scr_exc_pending()) return SIZE_MAX;
+ ScrMessageNode *node = &encoder->message->nodes[id];
+ ScrError *error = scr_errdyn_err_of(input);
+ if (!error && value != input) error = scr_errdyn_err_of(value);
+ if (error) {
+ node->kind = SCR_MESSAGE_ERROR;
+ node->number = SCR_ERR_ERROR;
+ static const char *const names[] = {
+ "Error", "TypeError", "RangeError", "SyntaxError", "ReferenceError", "EvalError", "URIError",
+ };
+ static const int kinds[] = {
+ SCR_ERR_ERROR, SCR_ERR_TYPE, SCR_ERR_RANGE, SCR_ERR_SYNTAX, SCR_ERR_REFERENCE, SCR_ERR_EVAL, SCR_ERR_URI,
+ };
+ for (size_t i = 0; i < sizeof names / sizeof names[0]; i++)
+ if (error->name->len == strlen(names[i]) && !memcmp(error->name->data, names[i], error->name->len))
+ node->number = kinds[i];
+ ScrDyn *keys = scr_dyn_new_arr();
+ static const char *const fields[] = {"message", "stack", "cause"};
+ for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) {
+ if (!scr_dyn_obj_get(value, fields[i], strlen(fields[i]))) continue;
+ ScrStr *key = scr_str_new(fields[i], strlen(fields[i]));
+ scr_dyn_arr_push(keys, scr_dyn_new_str(key));
+ scr_str_release(key);
+ }
+ encoder->visits[id].keys = keys;
+ scr_error_release(error);
+ scr_message_push(encoder, id);
+ return id;
+ }
+ switch (value->kind) {
+ case SCR_DYN_UNDEF: node->kind = SCR_MESSAGE_UNDEFINED; break;
+ case SCR_DYN_NULL: node->kind = SCR_MESSAGE_NULL; break;
+ case SCR_DYN_BOOL: node->kind = SCR_MESSAGE_BOOL; node->number = value->v.b; break;
+ case SCR_DYN_NUM: node->kind = SCR_MESSAGE_NUMBER; node->number = value->v.num; break;
+ case SCR_DYN_STR:
+ node->kind = SCR_MESSAGE_STRING;
+ node->length = value->v.str->len;
+ node->data = scr_message_copy(value->v.str->data, node->length);
+ break;
+ case SCR_DYN_BIGINT: {
+ ScrStr *text = scr_bigint_to_string(value->v.bigint, 10);
+ if (!text) return SIZE_MAX;
+ node->kind = SCR_MESSAGE_BIGINT;
+ node->length = text->len;
+ node->data = scr_message_copy(text->data, text->len);
+ scr_str_release(text);
+ break;
+ }
+ case SCR_DYN_ARR:
+ case SCR_DYN_OBJ:
+ node->kind = value->kind == SCR_DYN_ARR ? SCR_MESSAGE_ARRAY : SCR_MESSAGE_OBJECT;
+ node->length = value->kind == SCR_DYN_ARR ? value->v.arr.len : 0;
+ encoder->visits[id].keys = scr_dyn_own_keys(value);
+ if (!encoder->visits[id].keys) return SIZE_MAX;
+ scr_message_push(encoder, id);
+ break;
+ case SCR_DYN_BYTES:
+ case SCR_DYN_HANDLE: {
+ bool buffer = scr_buffer_storage_is(value);
+ if (value->kind == SCR_DYN_HANDLE && !buffer) goto unsupported;
+ const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes;
+ size_t storage = scr_message_storage(encoder, bytes);
+ /* Adding the storage node can reallocate the node array. */
+ node = &encoder->message->nodes[id];
+ node->kind = buffer ? SCR_MESSAGE_ARRAY_BUFFER : SCR_MESSAGE_BYTES;
+ node->storage = storage;
+ node->element = bytes->elem;
+ node->length = bytes->len;
+ node->offset = (size_t)(bytes->data - (bytes->backing ? bytes->backing : bytes)->data);
+ node->data_view = bytes->is_data_view;
+ break;
+ }
+ default:
+unsupported:
+ scr_throw_domex("DataCloneError", "The value could not be cloned.");
+ return SIZE_MAX;
+ }
+ return id;
+}
+
+static void scr_message_property(ScrMessageEncoder *encoder, size_t id,
+ const ScrStr *key, size_t value) {
+ ScrMessageNode *node = &encoder->message->nodes[id];
+ if (node->count == node->capacity) {
+ size_t capacity = scr_message_grow(node->capacity);
+ ScrMessageProperty *properties = scr_message_alloc(capacity, sizeof(*properties));
+ if (node->count) memcpy(properties, node->properties, node->count * sizeof(*properties));
+ free(node->properties);
+ node->properties = properties;
+ node->capacity = capacity;
+ }
+ node->properties[node->count++] = (ScrMessageProperty){
+ (char *)scr_message_copy(key->data, key->len), key->len, value,
+ };
+}
+
+ScrMessage *scr_message_encode(const ScrDyn *value) {
+ ScrMessage *message = scr_message_alloc(1, sizeof(*message));
+ ScrMessageEncoder encoder = {.message = message};
+ if (scr_message_value(&encoder, value) == SIZE_MAX) goto done;
+ while (encoder.depth && !scr_exc_pending()) {
+ size_t id = encoder.stack[encoder.depth - 1];
+ ScrMessageVisit *visit = &encoder.visits[id];
+ if (visit->cursor == visit->keys->v.arr.len) { encoder.depth--; continue; }
+ ScrDyn *key = visit->keys->v.arr.items[visit->cursor++];
+ if (key->kind != SCR_DYN_STR ||
+ (message->nodes[id].kind != SCR_MESSAGE_ERROR && !scr_dyn_property_is_enumerable(visit->view, key->v.str))) continue;
+ ScrDyn *child = scr_dyn_bag_get(visit->view, key->v.str, visit->view);
+ if (!child || scr_exc_pending()) { scr_dyn_release(child); break; }
+ size_t next = scr_message_value(&encoder, child);
+ scr_dyn_release(child);
+ if (next == SIZE_MAX) break;
+ scr_message_property(&encoder, id, key->v.str, next);
+ }
+done:
+ for (size_t i = 0; i < message->count; i++) {
+ ScrMessageVisit *visit = &encoder.visits[i];
+ scr_dyn_release(visit->keys);
+ if (visit->view != visit->input) scr_dyn_release(visit->view);
+ scr_dyn_release((ScrDyn *)visit->input);
+ }
+ free(encoder.visits);
+ free(encoder.identities);
+ free(encoder.stack);
+ if (scr_exc_pending()) { scr_message_free(message); return NULL; }
+ return message;
+}
+
+static size_t scr_message_array_index(const char *key, size_t length, size_t limit) {
+ if (!length || (length > 1 && key[0] == '0')) return SIZE_MAX;
+ size_t index = 0;
+ for (size_t i = 0; i < length; i++) {
+ unsigned digit = (unsigned char)key[i] - '0';
+ if (digit > 9 || index > (SIZE_MAX - digit) / 10) return SIZE_MAX;
+ index = index * 10 + digit;
+ }
+ return index < limit ? index : SIZE_MAX;
+}
+
+ScrDyn *scr_message_decode(const ScrMessage *message) {
+ ScrDyn **values = scr_message_alloc(message->count, sizeof(*values));
+ ScrBytes **storage = scr_message_alloc(message->count, sizeof(*storage));
+ for (size_t i = 0; i < message->count; i++) {
+ const ScrMessageNode *node = &message->nodes[i];
+ if (node->kind != SCR_MESSAGE_STORAGE) continue;
+ storage[i] = node->shared ? scr_shared_wrap(node->shared) : scr_bytes_new(SCR_BYTES_U8, (double)node->length);
+ if (!storage[i]) goto done;
+ if (!node->shared && node->length) memcpy(storage[i]->data, node->data, node->length);
+ }
+ for (size_t i = 0; i < message->count; i++) {
+ const ScrMessageNode *node = &message->nodes[i];
+ switch (node->kind) {
+ case SCR_MESSAGE_UNDEFINED: values[i] = scr_dyn_retain(scr_dyn_undefined()); break;
+ case SCR_MESSAGE_NULL: values[i] = scr_dyn_new_null(); break;
+ case SCR_MESSAGE_BOOL: values[i] = scr_dyn_new_bool(node->number != 0); break;
+ case SCR_MESSAGE_NUMBER: values[i] = scr_dyn_new_num(node->number); break;
+ case SCR_MESSAGE_STRING:
+ case SCR_MESSAGE_BIGINT: {
+ ScrStr *text = scr_str_new((const char *)node->data, node->length);
+ if (node->kind == SCR_MESSAGE_STRING) values[i] = scr_dyn_new_str(text);
+ else {
+ ScrBigInt *integer = scr_bigint_parse(text);
+ if (integer) { values[i] = scr_dyn_new_bigint(integer); scr_bigint_release(integer); }
+ }
+ scr_str_release(text);
+ break;
+ }
+ case SCR_MESSAGE_ARRAY:
+ values[i] = scr_dyn_new_arr();
+ for (size_t j = 0; j < node->length; j++) scr_dyn_arr_push_hole(values[i]);
+ break;
+ case SCR_MESSAGE_OBJECT: values[i] = scr_dyn_new_obj(); break;
+ case SCR_MESSAGE_ERROR: {
+ scr_error_set_traced();
+ ScrError *error = scr_error_new((int)node->number, NULL);
+ values[i] = scr_dyn_from_error(error);
+ scr_error_release(error);
+ break;
+ }
+ case SCR_MESSAGE_BYTES: {
+ ScrBytes *bytes = scr_bytes_buffer_view(storage[node->storage], node->element,
+ (double)node->offset, true, (double)node->length);
+ if (bytes) {
+ bytes->is_data_view = node->data_view;
+ values[i] = scr_dyn_new_bytes(bytes);
+ scr_bytes_release(bytes);
+ }
+ break;
+ }
+ case SCR_MESSAGE_ARRAY_BUFFER:
+ values[i] = scr_array_buffer_from_bytes(storage[node->storage]);
+ break;
+ case SCR_MESSAGE_STORAGE: continue;
+ }
+ if (!values[i] || scr_exc_pending()) goto done;
+ }
+ for (size_t i = 0; i < message->count; i++) {
+ const ScrMessageNode *node = &message->nodes[i];
+ for (size_t j = 0; j < node->count; j++) {
+ const ScrMessageProperty *property = &node->properties[j];
+ ScrDyn *value = scr_dyn_retain(values[property->value]);
+ if (node->kind == SCR_MESSAGE_ARRAY) {
+ size_t index = scr_message_array_index(property->key, property->length, node->length);
+ if (index != SIZE_MAX) {
+ values[i]->v.arr.items[index] = value;
+ values[i]->v.arr.presence[index] = 1;
+ continue;
+ }
+ if (!values[i]->v.arr.properties) values[i]->v.arr.properties = scr_dyn_new_obj();
+ scr_dyn_obj_set(values[i]->v.arr.properties, property->key, property->length, value);
+ } else scr_dyn_obj_set(values[i], property->key, property->length, value);
+ }
+ if (node->kind == SCR_MESSAGE_ERROR) {
+ for (size_t j = 0; j < values[i]->v.obj.len; j++) values[i]->v.obj.entries[j].enumerable = false;
+ ScrError *error = scr_errdyn_err_of(values[i]);
+ scr_error_commit_dyn(error, values[i]);
+ scr_error_release(error);
+ }
+ }
+done:;
+ ScrDyn *result = !scr_exc_pending() ? scr_dyn_retain(values[0]) : NULL;
+ for (size_t i = 0; i < message->count; i++) {
+ scr_dyn_release(values[i]);
+ scr_bytes_release(storage[i]);
+ }
+ free(values);
+ free(storage);
+ return result;
+}
+
+void scr_message_free(ScrMessage *message) {
+ if (!message) return;
+ for (size_t i = 0; i < message->count; i++) {
+ ScrMessageNode *node = &message->nodes[i];
+ for (size_t j = 0; j < node->count; j++) free(node->properties[j].key);
+ free(node->properties);
+ free(node->data);
+ scr_shared_release(node->shared);
+ }
+ free(message->nodes);
+ free(message);
+}
diff --git a/packages/runtime/src/scr_message.h b/packages/runtime/src/scr_message.h
new file mode 100644
index 000000000..29ed5f7a7
--- /dev/null
+++ b/packages/runtime/src/scr_message.h
@@ -0,0 +1,14 @@
+#ifndef SCR_MESSAGE_H
+#define SCR_MESSAGE_H
+
+#include "scr_runtime.h"
+
+/* Queues own messages independently of either runtime heap. Encoding may
+ * invoke getters and throw on the sender. Decoding creates recipient-local
+ * values; release the message separately after delivery or cancellation. */
+typedef struct ScrMessage ScrMessage;
+ScrMessage *scr_message_encode(const ScrDyn *value);
+ScrDyn *scr_message_decode(const ScrMessage *message);
+void scr_message_free(ScrMessage *message);
+
+#endif
diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c
new file mode 100644
index 000000000..5ce443c6f
--- /dev/null
+++ b/packages/runtime/src/scr_message.test.c
@@ -0,0 +1,152 @@
+#undef NDEBUG
+#include "scr_message.h"
+#include
+#include
+#include
+#include
+
+static void put_number(ScrDyn *object, const char *key, double value) {
+ scr_dyn_obj_set(object, key, strlen(key), scr_dyn_new_num(value));
+}
+
+static ScrDyn *make_graph(void) {
+ ScrDyn *root = scr_dyn_new_obj();
+ ScrDyn *shared = scr_dyn_new_obj();
+ put_number(shared, "value", 17);
+ scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(shared));
+ scr_dyn_obj_set(root, "second", 6, shared);
+ scr_dyn_obj_set(root, "self", 4, scr_dyn_retain(root));
+ put_number(root, "negativeZero", -0.0);
+ put_number(root, "nan", NAN);
+ put_number(root, "infinity", INFINITY);
+ ScrStr *text = scr_str_new("a\0\xf0\x9f\x98\x80", 6);
+ scr_dyn_obj_set(root, "text", 4, scr_dyn_new_str(text));
+ scr_str_release(text);
+ text = scr_str_new("123456789012345678901234567890", 30);
+ ScrBigInt *integer = scr_bigint_parse(text);
+ scr_str_release(text);
+ scr_dyn_obj_set(root, "integer", 7, scr_dyn_new_bigint(integer));
+ scr_bigint_release(integer);
+
+ ScrDyn *array = scr_dyn_new_arr();
+ scr_dyn_arr_push_hole(array);
+ scr_dyn_arr_push(array, scr_dyn_retain(scr_dyn_undefined()));
+ scr_dyn_arr_push_hole(array);
+ scr_dyn_arr_push(array, scr_dyn_retain(shared));
+ scr_dyn_obj_set(root, "array", 5, array);
+
+ ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, 16);
+ for (size_t i = 0; i < bytes->len; i++) bytes->data[i] = (unsigned char)i;
+ ScrBytes *view = scr_bytes_buffer_view(bytes, SCR_BYTES_U16, 4, true, 3);
+ scr_dyn_obj_set(root, "bytes", 5, scr_dyn_new_bytes(view));
+ scr_dyn_obj_set(root, "again", 5, scr_dyn_new_bytes(view));
+ scr_dyn_obj_set(root, "buffer", 6, scr_array_buffer_from_bytes(bytes));
+ scr_bytes_release(view);
+ scr_bytes_release(bytes);
+ return root;
+}
+
+static void *receive_graph(void *data) {
+ scr_context_enter(1);
+ scr_init();
+ scr_lib_init(0, NULL);
+ ScrDyn *root = scr_message_decode(data);
+ assert(root && !scr_exc_pending());
+ ScrDyn *first = scr_dyn_obj_get(root, "first", 5);
+ assert(first == scr_dyn_obj_get(root, "second", 6));
+ assert(root == scr_dyn_obj_get(root, "self", 4));
+ assert(signbit(scr_dyn_obj_get(root, "negativeZero", 12)->v.num));
+ assert(isnan(scr_dyn_obj_get(root, "nan", 3)->v.num));
+ assert(isinf(scr_dyn_obj_get(root, "infinity", 8)->v.num));
+ ScrStr *text = scr_dyn_obj_get(root, "text", 4)->v.str;
+ assert(text->len == 6 && !memcmp(text->data, "a\0\xf0\x9f\x98\x80", 6));
+ text = scr_bigint_to_string(scr_dyn_obj_get(root, "integer", 7)->v.bigint, 10);
+ assert(text->len == 30 && !memcmp(text->data, "123456789012345678901234567890", 30));
+ scr_str_release(text);
+ ScrDyn *array = scr_dyn_obj_get(root, "array", 5);
+ assert(array->v.arr.len == 4);
+ assert(!scr_dyn_arr_has_index(array, 0) && scr_dyn_arr_has_index(array, 1));
+ assert(!scr_dyn_arr_has_index(array, 2) && scr_dyn_arr_has_index(array, 3));
+ assert(array->v.arr.items[3] == first);
+ ScrDyn *bytes = scr_dyn_obj_get(root, "bytes", 5);
+ assert(bytes == scr_dyn_obj_get(root, "again", 5));
+ assert(bytes->v.bytes->elem == SCR_BYTES_U16 && bytes->v.bytes->len == 3);
+ assert(scr_bytes_byte_offset(bytes->v.bytes) == 4);
+ ScrBytes *buffer = scr_dyn_obj_get(root, "buffer", 6)->v.handle.ptr;
+ assert(bytes->v.bytes->backing == buffer && buffer->len == 16);
+ bytes->v.bytes->data[0] = 91;
+ assert(buffer->data[4] == 91);
+ scr_dyn_release(root);
+ scr_context_cleanup();
+ return NULL;
+}
+
+static SCR_TL char getters[4];
+static SCR_TL size_t getter_count;
+static ScrDyn *read_getter(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ (void)args; (void)argc;
+ double value = scr_box_get_f64(closure->caps[0]);
+ getters[getter_count++] = (char)value;
+ return scr_dyn_new_num(value);
+}
+
+static void define_getter(ScrDyn *object, const char *key, char marker) {
+ ScrClosure *closure = scr_closure_new(NULL, 1);
+ closure->caps[0] = scr_box_new(SCR_BOX_F64);
+ scr_box_set_f64(closure->caps[0], marker);
+ ScrDyn *descriptor = scr_dyn_new_obj();
+ scr_dyn_obj_set(descriptor, "get", 3, scr_dyn_new_func(closure, read_getter, 0, "", ""));
+ scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(true));
+ ScrStr *name = scr_str_new(key, strlen(key));
+ ScrDyn *name_value = scr_dyn_new_str(name);
+ ScrDyn *result = scr_dyn_define_property(object, name_value, descriptor);
+ assert(result && !scr_exc_pending());
+ scr_dyn_release(result);
+ scr_dyn_release(name_value);
+ scr_dyn_release(descriptor);
+ scr_str_release(name);
+}
+
+static void check_getters(void) {
+ ScrDyn *root = scr_dyn_new_obj();
+ ScrDyn *nested = scr_dyn_new_obj();
+ define_getter(nested, "inner", 'a');
+ scr_dyn_obj_set(root, "first", 5, nested);
+ define_getter(root, "last", 'b');
+ ScrMessage *message = scr_message_encode(root);
+ assert(message && !scr_exc_pending());
+ assert(getter_count == 2 && !memcmp(getters, "ab", 2));
+ scr_message_free(message);
+ scr_dyn_release(root);
+ scr_collect_cycles();
+}
+
+static void check_rejection(void) {
+ ScrDyn *root = scr_dyn_new_obj();
+ ScrClosure *closure = scr_closure_new(NULL, 0);
+ ScrDyn *function = scr_dyn_new_func(closure, read_getter, 0, "", "");
+ scr_dyn_obj_set(root, "function", 8, function);
+ assert(!scr_message_encode(root) && scr_exc_pending());
+ scr_exc_clear();
+ scr_dyn_release(root);
+ scr_collect_cycles();
+}
+
+int main(void) {
+ scr_init();
+ scr_lib_init(0, NULL);
+ ScrDyn *source = make_graph();
+ ScrMessage *message = scr_message_encode(source);
+ assert(message && !scr_exc_pending());
+ scr_dyn_release(source);
+ /* The sender's graph is gone before any recipient starts decoding. */
+ scr_collect_cycles();
+ pthread_t receiver;
+ assert(pthread_create(&receiver, NULL, receive_graph, message) == 0);
+ assert(pthread_join(receiver, NULL) == 0);
+ scr_message_free(message);
+ check_getters();
+ check_rejection();
+ scr_context_cleanup();
+ puts("message graphs preserve identity, sparse values, byte aliases and getter order");
+}
diff --git a/packages/runtime/src/scr_message.test.ts b/packages/runtime/src/scr_message.test.ts
new file mode 100644
index 000000000..b2364a5d8
--- /dev/null
+++ b/packages/runtime/src/scr_message.test.ts
@@ -0,0 +1,44 @@
+import { execFile } from "node:child_process";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { promisify } from "node:util";
+import { expect, test } from "vitest";
+import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js";
+
+const exec = promisify(execFile);
+
+test.skipIf(process.platform === "win32")(
+ "message data outlives its sender and decodes into a separate runtime heap",
+ async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-message-"));
+ try {
+ const bin = join(dir, "messages");
+ await exec("clang", [
+ "-std=c11",
+ "-O1",
+ "-pthread",
+ "-DSCR_WORKERS",
+ "-DSCR_RC_AUDIT",
+ "-fsanitize=address,undefined",
+ "-Wno-deprecated-declarations",
+ ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
+ join(import.meta.dirname, "scr_message.test.c"),
+ join(import.meta.dirname, "scr_message.c"),
+ ...EXECUTABLE_RUNTIME_SOURCES.map((name) => join(import.meta.dirname, name)),
+ ...(process.platform === "linux" ? ["-lm"] : []),
+ "-o",
+ bin,
+ ]);
+ const result = await exec(bin, [], {
+ env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" },
+ });
+ expect(result.stdout).toBe(
+ "message graphs preserve identity, sparse values, byte aliases and getter order\n",
+ );
+ expect(result.stderr).toBe("");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ },
+);
diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c
index 5447db02b..0c9c60d85 100644
--- a/packages/runtime/src/scr_net.c
+++ b/packages/runtime/src/scr_net.c
@@ -609,13 +609,13 @@ struct ScrNetSocket {
};
#ifdef SCR_RC_AUDIT
-static long scr_net_live = 0;
+static SCR_TL long scr_net_live = 0;
long scr_net_live_count(void) { return scr_net_live; }
#endif
-static ScrNetServer *scr_net_servers = NULL; /* registry: +1 each, tail-appended */
-static ScrNetSocket *scr_net_socks = NULL;
-static ScrPoller *scr_net_poller = NULL;
+static SCR_TL ScrNetServer *scr_net_servers = NULL; /* registry: +1 each, tail-appended */
+static SCR_TL ScrNetSocket *scr_net_socks = NULL;
+static SCR_TL ScrPoller *scr_net_poller = NULL;
/* ── RC ──────────────────────────────────────────────────────────────── */
@@ -1649,7 +1649,7 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab
scr_net_ls_drop(&s->listening_cbs);
}
if (!s->closing) {
- static size_t scr_net_close_seq = 0;
+ static SCR_TL size_t scr_net_close_seq = 0;
s->close_seq = ++scr_net_close_seq;
}
s->closing = true; /* the sweep fires 'close' once nconns drains */
@@ -2222,6 +2222,7 @@ void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/) {
}
void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
scr_net_sock_write_raw(s, (const char *)data->data, (data->len * scr_bytes_elem_size(data->elem)));
}
@@ -2430,6 +2431,7 @@ ScrBytes *scr_net_sock_read_bytes(ScrNetSocket *s, double n) {
* buffer — every consumer (data listeners, an http parser, a TLS bio)
* sees them before anything still in the kernel. */
void scr_net_sock_unshift_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/) {
+ SCR_BYTES_SNAPSHOT(data);
if (data->len == 0) return;
size_t avail = s->rlen - s->rhead;
if (s->rhead >= data->len) {
@@ -2784,9 +2786,9 @@ void scr_net_server_get_native_conn(ScrNetServer *s, ScrNetNativeConnFn *fn, voi
* pass after the work that flagged them — Node's later-than-the-handler
* emits). Registered once by scr_http install-time code. */
enum { SCR_NET_PROTO_SLOTS = 4 };
-static bool (*scr_net_proto_pendings[SCR_NET_PROTO_SLOTS])(void);
-static void (*scr_net_proto_sweeps[SCR_NET_PROTO_SLOTS])(void);
-static size_t scr_net_proto_n = 0;
+static SCR_TL bool (*scr_net_proto_pendings[SCR_NET_PROTO_SLOTS])(void);
+static SCR_TL void (*scr_net_proto_sweeps[SCR_NET_PROTO_SLOTS])(void);
+static SCR_TL size_t scr_net_proto_n = 0;
/* ADDITIVE registration (each protocol unit — scr_http.c, scr_http2.c —
* claims a slot at its install; both can co-link in one binary). */
@@ -3180,10 +3182,10 @@ static void scr_net_cleanup_atexit(void) {
}
void scr_net_install(void) {
- static bool installed = false;
+ static SCR_TL bool installed = false;
if (installed) return;
installed = true;
- atexit(scr_net_cleanup_atexit);
+ scr_atexit(scr_net_cleanup_atexit);
scr_loop_set_net(&scr_net_pending, &scr_net_dispatch, &scr_net_pollfd);
}
@@ -3204,7 +3206,7 @@ static void scr_net_dynh_unsupported(const char *member, const char *why) {
/* The TLS-member hooks (scr_tls.c registers them when linked): TLSSocket
* property reads, event names, and method names layer over the plain
* socket dispatch without this unit naming that one's symbols. */
-static const ScrNetDynhTlsHooks *scr_net_dynh_tls = NULL;
+static SCR_TL const ScrNetDynhTlsHooks *scr_net_dynh_tls = NULL;
void scr_net_set_dynh_tls(const ScrNetDynhTlsHooks *hooks) { scr_net_dynh_tls = hooks; }
static bool scr_net_dynh_name_is(const ScrDyn *name, const char *lit) {
@@ -3457,7 +3459,7 @@ static const ScrDynHandleOps scr_net_dynh_sock_ops = {
* at its dyn install — this unit cannot name http entry points without
* breaking the link gate. */
-static bool (*scr_net_dynh_http_on)(ScrNetServer *s, const char *event, const ScrDyn *cb,
+static SCR_TL bool (*scr_net_dynh_http_on)(ScrNetServer *s, const char *event, const ScrDyn *cb,
bool once);
void scr_net_set_dynh_http_on(bool (*fn)(ScrNetServer *, const char *, const ScrDyn *, bool)) {
scr_net_dynh_http_on = fn;
diff --git a/packages/runtime/src/scr_path.c b/packages/runtime/src/scr_path.c
index 211452cb2..019680103 100644
--- a/packages/runtime/src/scr_path.c
+++ b/packages/runtime/src/scr_path.c
@@ -845,7 +845,7 @@ ScrStr *scr_path_win32_resolve(ScrArr *parts) {
* the resolved drive. resolvedDevice is always a 2-byte drive here
* (UNC devices are absolute and broke out of the loop). */
char name[4] = {'=', dev.data[0], dev.data[1], 0};
- const char *dcwd = getenv(name);
+ const char *dcwd = scr_getenv(name);
if (dcwd && *dcwd) pb_append(&pathb, dcwd, strlen(dcwd));
else scr_path_cwd(&pathb);
if (!(pathb.len >= 2 && scr_path_w32_ieq(pathb.data, dev.data, 2)) &&
diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c
index ef06e8d84..134ba48c3 100644
--- a/packages/runtime/src/scr_readline.c
+++ b/packages/runtime/src/scr_readline.c
@@ -48,12 +48,12 @@ typedef struct ScrRl {
struct ScrRl *next;
} ScrRl;
-static ScrRl *scr_rls = NULL;
-static double scr_rl_next_id = 1;
-static size_t scr_rl_open = 0; /* un-closed, un-dead interfaces */
-static ScrClosure *scr_rl_data_cb = NULL; /* the shared consumer (borrowed
+static SCR_TL ScrRl *scr_rls = NULL;
+static SCR_TL double scr_rl_next_id = 1;
+static SCR_TL size_t scr_rl_open = 0; /* un-closed, un-dead interfaces */
+static SCR_TL ScrClosure *scr_rl_data_cb = NULL; /* the shared consumer (borrowed
* mirror; the registry owns) */
-static bool scr_rl_end_registered = false;
+static SCR_TL bool scr_rl_end_registered = false;
static ScrRl *scr_rl_find(double id) {
for (ScrRl *rl = scr_rls; rl; rl = rl->next) {
@@ -146,7 +146,7 @@ static void scr_rl_data_adapter(ScrClosure *cb, ScrBytes *chunk) {
rl->buf = realloc(rl->buf, rl->cap);
if (!rl->buf) scr_rl_oom();
}
- memcpy(rl->buf + rl->len, chunk->data, chunk->len);
+ scr_bytes_read(chunk, 0, rl->buf + rl->len, chunk->len);
rl->len += chunk->len;
scr_rl_drain(rl);
if (scr_exc_pending()) return;
@@ -197,10 +197,10 @@ static void scr_rl_cleanup_atexit(void) {
}
double scr_rl_create(void) {
- static bool cleanup_registered = false;
+ static SCR_TL bool cleanup_registered = false;
if (!cleanup_registered) {
cleanup_registered = true;
- atexit(scr_rl_cleanup_atexit);
+ scr_atexit(scr_rl_cleanup_atexit);
}
ScrRl *rl = calloc(1, sizeof *rl);
if (!rl) scr_rl_oom();
diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h
index 9be7ce57d..c78e34560 100644
--- a/packages/runtime/src/scr_runtime.h
+++ b/packages/runtime/src/scr_runtime.h
@@ -35,7 +35,7 @@ char *strcasestr(const char *hay, const char *needle);
void arc4random_buf(void *buf, size_t n);
#endif
-/* ── thread-instanced library state ─────────────────────────────────────
+/* ── runtime instance state ─────────────────────────────────────────────
* Archives built under the profile's abi.instance_per_thread compile every
* TU with -DSCR_THREAD_INSTANCES, and SCR_TL moves each unit's mutable
* state — and the emitted program's globals — into thread-local storage.
@@ -44,10 +44,10 @@ void arc4random_buf(void *buf, size_t n);
* registration, poison flag, and program state. The instance's lifetime is
* the thread's; the one-thread-per-instance contract (an instance is never
* entered from two threads) is unchanged — this mode adds instances, not
- * thread awareness. Expands to nothing everywhere else, so executable
- * builds and classic library builds carry the exact bytes they always
- * carried. Truly immutable tables (static const data) stay shared. */
-#if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
+ * thread awareness. Worker executables use the same isolation for their
+ * collectors, caches and scheduler state. Expands to nothing for ordinary
+ * executables and classic libraries. Immutable tables stay shared. */
+#if (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS)
#define SCR_TL _Thread_local
#else
#define SCR_TL
@@ -62,7 +62,8 @@ void scr_init(void);
/* Program objects emitted by the bundled LLVM helper reference this symbol.
* Its versioned spelling makes a mismatched manual runtime link fail before
* the program can start. */
-void scr_runtime_abi_v6(void);
+void scr_runtime_abi_v7(void);
+void scr_runtime_workers_v7(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
* Every unrecoverable runtime trap — OOM, semantic range traps, internal-
@@ -276,10 +277,45 @@ void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len);
void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len);
#define scr_atexit(fn) scr_library_register_reset(fn)
+#elif defined(SCR_WORKERS)
+/* Executable workers drain their own cleanup stack before their TLS dies.
+ * Registering their callbacks with libc would execute them on the process
+ * exit thread against a different runtime instance. */
+int scr_context_atexit(void (*fn)(void));
+void scr_context_cleanup(void);
+void scr_context_enter(uint64_t thread_id);
+uint64_t scr_context_thread_id(void);
+void scr_context_stop_flag(const void *flag);
+void scr_context_stop(int code);
+bool scr_context_stopping(void);
+bool scr_context_checkpoint(void);
+void scr_loop_context_shutdown(void);
+extern SCR_TL void (*scr_context_report_error)(void);
+typedef struct ScrContextEnv ScrContextEnv;
+ScrContextEnv *scr_context_env_capture(void);
+void scr_context_env_free(ScrContextEnv *environment);
+void scr_context_env_enter(ScrContextEnv *environment);
+bool scr_context_env_set(const char *name, const char *value);
+const char *scr_context_getenv(const char *name);
+struct ScrArr *scr_context_env_pairs(void);
+#define scr_atexit(fn) scr_context_atexit(fn)
#else
#define scr_atexit(fn) atexit(fn)
#endif /* SCR_LIB */
+/* Also used by ordinary event-loop builds, where this always returns true. */
+bool scr_context_is_main(void);
+double scr_context_thread_number(void);
+double scr_worker_root(void);
+int scr_worker_argc(void);
+char **scr_worker_argv(void);
+
+#ifdef SCR_WORKERS
+#define scr_getenv(name) scr_context_getenv(name)
+#else
+#define scr_getenv(name) getenv(name)
+#endif
+
/* ── cycle collection (scr_cycle.c) ───────────────────────────────────
* Reference counting alone cannot free cycles, so every object that can
* participate in one — capture boxes, heap closures, unions, promises, and
@@ -403,6 +439,7 @@ static inline void scr_cyc_mark_live(void *obj) {
* after and wants nothing reclaimable left), and it costs a walk of the
* live heap — do not put it on a per-turn path. */
void scr_collect_cycles(void);
+void scr_cyc_context_cleanup(void);
/* One pass on the normal generational schedule, for callers that reach a
* natural collection point rather than a threshold (the event loop between
@@ -1884,6 +1921,9 @@ extern SCR_TL ScrVt scr_emitter_vt; /* main() stamps pre/post */
extern SCR_TL double scr_emitter_default_max;
ScrEmitter *scr_emitter_new(void); /* +1, collector-headered */
+/* Internal owner hook; observes listener presence without running script. */
+void scr_emitter_observe(ScrEmitter *em,
+ void (*observe)(void *, const ScrStr *, bool), void *context);
void scr_emitter_init(void *obj); /* super() into the prefix (no-op today) */
ScrEmitter *scr_emitter_retain(ScrEmitter *em);
void scr_emitter_release(ScrEmitter *em); /* dispatches through em->vt */
@@ -1961,6 +2001,7 @@ ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all);
* emit('error', err): no listener ⇒ THROWS err (borrowed; +1 taken). */
bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...);
bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc);
+bool scr_emitter_emit_error_flex(ScrEmitter *em, ScrDyn *error);
bool scr_emitter_emit_error(ScrEmitter *em, ScrStr *name, ScrError *err);
double scr_emitter_listener_count(ScrEmitter *em, ScrStr *name);
double scr_emitter_listener_count_fn(ScrEmitter *em, ScrStr *name, ScrClosure *fn);
@@ -1982,7 +2023,7 @@ bool scr_emitter_has(ScrEmitter *em, const char *name);
* the END of scr_emitter_on with the emitter and the event name, so the
* stream unit can start flowing on the first 'data' listener. NULL when
* the stream unit is not linked; emitter behavior is byte-identical. */
-extern void (*scr_emitter_on_hook)(ScrEmitter *em, ScrStr *name);
+extern SCR_TL void (*scr_emitter_on_hook)(ScrEmitter *em, ScrStr *name);
/* ── node:stream (scr_stream.c, link-gated by moduleUsesStream) ───────
* The runtime-provided stream classes (Readable/Writable/Duplex/
@@ -2023,7 +2064,7 @@ typedef struct ScrStream {
ScrStreamState *st;
} ScrStream;
-extern ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt,
+extern SCR_TL ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt,
scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */
/* Compiler-emitted option-callback invoke adapters (the leading-`this`
@@ -2334,6 +2375,9 @@ typedef enum {
* functions, and checked-dynamic primitives). The payload uses the
* same ownership as REF. */
SCR_EXC_PRIMITIVE_REF,
+ /* Noncatchable context shutdown. Owns no payload; generated unwinding
+ * releases locals while bypassing user catch and finally handlers. */
+ SCR_EXC_TERMINATE,
} ScrExcKind;
/* One cell per fiber (JS has one exception in flight per execution
@@ -2451,7 +2495,7 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c);
void scr_exc_print_uncaught(void);
/* Optional native process exception registry. Hook returns 1 when handled,
* 0 when unhandled, and -1 when a handler itself threw (Node exit 7). */
-extern int (*scr_uncaught_exception_hook)(bool from_promise);
+extern SCR_TL int (*scr_uncaught_exception_hook)(bool from_promise);
bool scr_exc_handle_uncaught(bool from_promise);
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor);
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor);
@@ -2513,7 +2557,7 @@ ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */
* internal stdio formatting buffer never delays live output. */
bool scr_stdio_write(int fd, const void *data, size_t len);
int scr_stdio_write_raw(int fd, const void *data, size_t len);
-extern bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len);
+extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len);
/* process.stdout/.stderr .write — raw bytes (no newline or formatting; data
* borrowed), promptly visible and ordered with console output. Constantly
* true (the synchronous runtime never queues backpressure). */
@@ -2676,15 +2720,11 @@ void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode);
void scr_fs_mkdir_mode(ScrStr *path, double mode);
void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode);
-/* Atomics.wait(int32Array, idx, expected, timeoutMs): "not-equal" when
- * the element differs from expected, else a real nanosleep for the
- * timeout and "timed-out" (+1 string either way). scriptc has no
- * threads, so nothing can ever notify — for every compilable program
- * this IS the spec's behavior and "ok" is unreachable (the compiler
- * requires the timeout argument; an infinite wait would be a certain
- * deadlock). Out-of-range indices trap like every bytes access. */
-typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
-ScrStr *scr_atomics_wait(ScrBytes *arr, double idx, double expected, double timeout_ms);
+/* Sequentially consistent integer operations and shared Int32 wait queues. */
+typedef struct ScrBytes ScrBytes;
+ScrStr *scr_atomics_wait(ScrBytes *, double, double, double);
+double scr_atomics_notify(ScrBytes *, double, double);
+double scr_atomics_op(ScrBytes *, double, double, double, double);
/* isatty(3) over an fd (0/1/2): a real boolean — false where Node's
* non-TTY streams expose undefined (SEMANTICS.md). stdin_destroy is a
@@ -3709,6 +3749,9 @@ typedef enum {
SCR_DYNH_CONSOLE, /* stored global console value */
SCR_DYNH_SEARCH_PARAMS, /* live WHATWG URLSearchParams */
SCR_DYNH_FETCH_REQUEST,
+ SCR_DYNH_WORKER,
+ SCR_DYNH_MESSAGE_PORT,
+ SCR_DYNH_SHARED_ARRAY_BUFFER,
SCR_DYNH_COUNT,
} ScrDynHandleTag;
@@ -4386,7 +4429,7 @@ ScrDyn *scr_dyn_new_promise(ScrPromise *p); /* scr_async_dyn.c (gated) */
/* The allocator view the gated boxes use; installs the release arm's
* promise-release edge (runtime-internal). */
ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p));
-extern void (*scr_dyn_promise_release_fn)(ScrPromise *p);
+extern SCR_TL void (*scr_dyn_promise_release_fn)(ScrPromise *p);
ScrDyn *scr_dyn_new_promise_adapting(ScrPromise *src,
void (*adapt)(ScrPromise *dst, ScrPromise *src));
/* BORROWED peek at the boxed promise; NULL when d is not a promise box. */
@@ -4761,7 +4804,13 @@ typedef struct ScrAlsCtx {
ScrAlsEntry entries[]; /* flexible */
} ScrAlsCtx;
/* Runtime-internal (the fiber machinery's always-linked core). */
-extern ScrAlsCtx **scr_als_active;
+extern SCR_TL ScrAlsCtx **scr_als_active;
+#ifdef SCR_WORKERS
+ScrAlsCtx **scr_als_slot(void);
+#define SCR_ALS_SLOT() scr_als_slot()
+#else
+#define SCR_ALS_SLOT() scr_als_active
+#endif
ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c);
void scr_als_ctx_release(ScrAlsCtx *c);
double scr_als_new(void);
@@ -4792,12 +4841,12 @@ void scr_process_off_rejection_handled(ScrDyn *fn);
/* The checkpoint delivery hook the registration above installs
* (scr_async_dyn.c → scr_report_unhandled_rejections; NULL = default
* report). Runtime-internal. */
-extern bool (*scr_urj_deliver_fn)(ScrPromise *p);
+extern SCR_TL bool (*scr_urj_deliver_fn)(ScrPromise *p);
/* The late-handled hook (scr_async_dyn.c installs it when a
* 'rejectionHandled' listener registers): scr_async.c calls it when a
* promise the report already delivered as unhandled gains a handler.
* Runtime-internal. */
-extern void (*scr_rjh_notify_fn)(ScrPromise *p);
+extern SCR_TL void (*scr_rjh_notify_fn)(ScrPromise *p);
/* The attach-time handled mark (a dyn then/catch carrying a rejection
* handler, or the module loader taking ownership of an evaluation
* promise): marks pending and rejected sources observed at Node's attach
@@ -5028,8 +5077,8 @@ void scr_stdin_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk);
* not the unit is present. */
void scr_loop_set_events(bool (*pending)(void), bool (*watching)(void),
void (*dispatch)(void), int (*pollfds)(int out[2]));
-extern void (*scr_process_exit_hook)(double code);
-extern void (*scr_stdin_destroy_hook)(void);
+extern SCR_TL void (*scr_process_exit_hook)(double code);
+extern SCR_TL void (*scr_stdin_destroy_hook)(void);
void scr_exit_code_note(int code);
int scr_exit_code_hint_get(void);
/* The pollable child-exit wake fd (the child kqueue / pidfd epoll) for the loop's
@@ -5820,6 +5869,8 @@ typedef enum ScrBytesElem {
SCR_BYTES_U8C, /* Uint8ClampedArray */
} ScrBytesElem;
+typedef struct ScrSharedBytes ScrSharedBytes;
+
typedef struct ScrBytes {
size_t rc;
size_t len; /* ELEMENT count, fixed at construction */
@@ -5833,8 +5884,46 @@ typedef struct ScrBytes {
bool is_buffer; /* Buffer brand belongs to the view, not its backing. */
bool is_data_view; /* DataView is distinct from a numeric typed array. */
bool external; /* Root aliases foreign memory; releasing it never frees data. */
+ ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */
} ScrBytes;
+/* Internal scoped locking is only for leaf memory operations. Never hold this
+ * guard across a script callback, blocking I/O, or a fiber suspension. */
+void scr_shared_enter(void);
+void scr_shared_leave(void);
+bool scr_shared_guard(const ScrBytes *, const ScrBytes *);
+void scr_shared_guard_release(bool *);
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+#define SCR_SHARED_GUARD(a, b) bool scr_shared_held __attribute__((cleanup(scr_shared_guard_release))) = scr_shared_guard(a, b)
+#else
+#define SCR_SHARED_GUARD(a, b) ((void)0)
+#endif
+ScrSharedBytes *scr_shared_retain(ScrSharedBytes *);
+void scr_shared_release(ScrSharedBytes *);
+void scr_bytes_make_shared(ScrBytes *);
+ScrBytes *scr_shared_wrap(ScrSharedBytes *);
+void scr_bytes_read(const ScrBytes *, size_t, void *, size_t);
+void scr_bytes_write(ScrBytes *, size_t, const void *, size_t);
+ScrStr *scr_bytes_string(const ScrBytes *, size_t, size_t);
+ScrBytes *scr_bytes_snapshot(const ScrBytes *);
+void scr_bytes_snapshot_release(ScrBytes **);
+ScrBytes *scr_bytes_local_copy(ScrBytes *); /* +1; shared inputs are copied */
+void scr_bytes_require_unshared(const ScrBytes *);
+/* Snapshot before passing byte pointers to I/O or callback-capable consumers.
+ * The copy is local, so neither a lock nor shared raw memory escapes. */
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+#define SCR_BYTES_SNAPSHOT(name) \
+ ScrBytes *scr_snapshot_##name __attribute__((cleanup(scr_bytes_snapshot_release))) = scr_bytes_snapshot(name); \
+ if (scr_snapshot_##name) name = scr_snapshot_##name
+#else
+#define SCR_BYTES_SNAPSHOT(name) ((void)0)
+#endif
+ScrDyn *scr_shared_array_buffer_new(ScrDyn *);
+ScrDyn *scr_worker_data(void);
+ScrDyn *scr_worker_parent_port(void);
+ScrDyn *scr_worker_new(double root, ScrStr *filename, ScrDyn *options);
+bool scr_shared_array_buffer_is(const ScrDyn *);
+bool scr_buffer_storage_is(const ScrDyn *);
ScrBytes *scr_bytes_from_external(void *data, size_t length);
ScrDyn *scr_ffi_memory_module(ScrDyn *catalog);
ScrDyn *scr_ffi_argument(ScrDyn *value, ScrStr *type);
diff --git a/packages/runtime/src/scr_shared.c b/packages/runtime/src/scr_shared.c
new file mode 100644
index 000000000..82e33b6ca
--- /dev/null
+++ b/packages/runtime/src/scr_shared.c
@@ -0,0 +1,280 @@
+/* Shared storage is neutral: only its backing allocation and synchronization
+ * cross contexts. ScrBytes wrappers and their reference counts remain local. */
+#include "scr_runtime.h"
+#include "scr_numeric.h"
+#include
+#include
+#include
+#include
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+#ifdef _WIN32
+#include
+#else
+#include
+#include
+#endif
+#endif
+
+struct ScrSharedBytes {
+ atomic_size_t references;
+ size_t length;
+ uint8_t *data;
+};
+
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+#ifdef _WIN32
+static SRWLOCK scr_shared_mutex = SRWLOCK_INIT;
+static CONDITION_VARIABLE scr_shared_condition = CONDITION_VARIABLE_INIT;
+#else
+static pthread_mutex_t scr_shared_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t scr_shared_condition = PTHREAD_COND_INITIALIZER;
+#endif
+static SCR_TL unsigned scr_shared_depth;
+#endif
+
+/* One ordering domain covers overlapping views of different widths. Recursive
+ * runtime helpers may nest, but no caller may execute script while holding it. */
+void scr_shared_enter(void) {
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+ if (scr_shared_depth++ != 0) return;
+#ifdef _WIN32
+ AcquireSRWLockExclusive(&scr_shared_mutex);
+#else
+ pthread_mutex_lock(&scr_shared_mutex);
+#endif
+#endif
+}
+
+void scr_shared_leave(void) {
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+ if (--scr_shared_depth != 0) return;
+#ifdef _WIN32
+ ReleaseSRWLockExclusive(&scr_shared_mutex);
+#else
+ pthread_mutex_unlock(&scr_shared_mutex);
+#endif
+#endif
+}
+
+bool scr_shared_guard(const ScrBytes *a, const ScrBytes *b) {
+ bool shared = (a && a->shared) || (b && b->shared);
+ if (shared) scr_shared_enter();
+ return shared;
+}
+
+void scr_shared_guard_release(bool *held) { if (*held) scr_shared_leave(); }
+
+ScrSharedBytes *scr_shared_retain(ScrSharedBytes *storage) {
+ atomic_fetch_add_explicit(&storage->references, 1, memory_order_relaxed);
+ return storage;
+}
+
+void scr_shared_release(ScrSharedBytes *storage) {
+ if (!storage || atomic_fetch_sub_explicit(&storage->references, 1, memory_order_acq_rel) != 1) return;
+ free(storage->data);
+ free(storage);
+}
+
+void scr_bytes_make_shared(ScrBytes *bytes) {
+ if (bytes->backing || bytes->shared || bytes->external) scr_trap("scriptc: invalid shared storage owner\n");
+ ScrSharedBytes *storage = malloc(sizeof(*storage));
+ if (!storage) scr_trap("scriptc: out of memory\n");
+ atomic_init(&storage->references, 1);
+ storage->length = bytes->len * scr_bytes_elem_size(bytes->elem);
+ storage->data = bytes->data;
+ bytes->shared = storage;
+}
+
+ScrBytes *scr_shared_wrap(ScrSharedBytes *storage) {
+ ScrBytes *bytes = scr_bytes_from_external(storage->data, storage->length);
+ bytes->external = false;
+ bytes->shared = scr_shared_retain(storage);
+ return bytes;
+}
+
+void scr_bytes_read(const ScrBytes *bytes, size_t offset, void *out, size_t length) {
+ SCR_SHARED_GUARD(bytes, NULL);
+ if (length) memcpy(out, bytes->data + offset, length);
+}
+
+void scr_bytes_write(ScrBytes *bytes, size_t offset, const void *data, size_t length) {
+ SCR_SHARED_GUARD(bytes, NULL);
+ if (length) memcpy(bytes->data + offset, data, length);
+}
+
+ScrStr *scr_bytes_string(const ScrBytes *bytes, size_t offset, size_t length) {
+ SCR_SHARED_GUARD(bytes, NULL);
+ return scr_str_new((const char *)bytes->data + offset, length);
+}
+
+ScrBytes *scr_bytes_snapshot(const ScrBytes *bytes) {
+ if (!bytes || !bytes->shared) return NULL;
+ ScrBytes *copy = scr_bytes_copy(bytes);
+ copy->is_buffer = bytes->is_buffer;
+ copy->is_data_view = bytes->is_data_view;
+ return copy;
+}
+
+void scr_bytes_snapshot_release(ScrBytes **bytes) { scr_bytes_release(*bytes); }
+
+ScrBytes *scr_bytes_local_copy(ScrBytes *bytes) {
+ ScrBytes *copy = scr_bytes_snapshot(bytes);
+ return copy ? copy : scr_bytes_retain(bytes);
+}
+
+void scr_bytes_require_unshared(const ScrBytes *bytes) {
+ if (!bytes->shared) return;
+ static const char message[] = "Shared byte storage cannot expose an unmanaged native pointer";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
+}
+
+static bool scr_atomics_index(ScrBytes *bytes, double index, bool waitable, size_t *out) {
+ bool integer = bytes->elem == SCR_BYTES_I8 || bytes->elem == SCR_BYTES_U8 ||
+ bytes->elem == SCR_BYTES_I16 || bytes->elem == SCR_BYTES_U16 ||
+ bytes->elem == SCR_BYTES_I32 || bytes->elem == SCR_BYTES_U32;
+ if (bytes->is_data_view || !integer || (waitable && bytes->elem != SCR_BYTES_I32)) {
+ static const char message[] = "The typed array is not an integer typed array of the required kind";
+ scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
+ return false;
+ }
+ double normalized = isnan(index) ? 0 : trunc(index);
+ if (!(normalized >= 0) || normalized >= (double)bytes->len) {
+ static const char message[] = "Invalid atomic access index";
+ scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
+ return false;
+ }
+ *out = (size_t)normalized;
+ return true;
+}
+
+/* Operations: load, store, exchange, compareExchange, add, sub, and, or, xor.
+ * Unsigned arithmetic avoids signed overflow; the element store supplies the
+ * selected width and signedness. The lock gives one sequentially consistent
+ * order shared with ordinary accesses, including overlapping DataViews. */
+double scr_atomics_op(ScrBytes *bytes, double index, double value, double replacement, double operation) {
+ size_t i;
+ if (!scr_atomics_index(bytes, index, false, &i)) return 0;
+ SCR_SHARED_GUARD(bytes, NULL);
+ double previous = scr_bytes_get(bytes, (double)i);
+ uint32_t before = scr_numeric_to_u32(previous), operand = scr_numeric_to_u32(value);
+ uint32_t after;
+ switch ((int)operation) {
+ case 0: return previous;
+ case 1:
+ scr_bytes_set(bytes, (double)i, value);
+ return isnan(value) || value == 0 ? 0 : trunc(value);
+ case 2: after = operand; break;
+ case 3: {
+ unsigned bits = (unsigned)scr_bytes_elem_size(bytes->elem) * 8;
+ uint32_t mask = bits == 32 ? UINT32_MAX : (UINT32_C(1) << bits) - 1;
+ if ((before & mask) != (operand & mask)) return previous;
+ after = scr_numeric_to_u32(replacement);
+ break;
+ }
+ case 4: after = before + operand; break;
+ case 5: after = before - operand; break;
+ case 6: after = before & operand; break;
+ case 7: after = before | operand; break;
+ case 8: after = before ^ operand; break;
+ default: scr_trap("scriptc: invalid atomic operation\n");
+ }
+ scr_bytes_set(bytes, (double)i, (double)after);
+ return previous;
+}
+
+typedef struct ScrAtomicWaiter {
+ ScrSharedBytes *storage;
+ size_t offset;
+ bool notified;
+ struct ScrAtomicWaiter *next;
+} ScrAtomicWaiter;
+static ScrAtomicWaiter *scr_atomic_waiters;
+
+static void scr_atomics_pause(double ms) {
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+#ifdef _WIN32
+ SleepConditionVariableSRW(&scr_shared_condition, &scr_shared_mutex, (DWORD)ceil(ms), 0);
+#else
+ struct timespec until;
+ clock_gettime(CLOCK_REALTIME, &until);
+ until.tv_nsec += (long)(ms * 1000000.0);
+ until.tv_sec += until.tv_nsec / 1000000000;
+ until.tv_nsec %= 1000000000;
+ pthread_cond_timedwait(&scr_shared_condition, &scr_shared_mutex, &until);
+#endif
+#else
+ struct timespec duration = {(time_t)(ms / 1000), (long)(fmod(ms, 1000) * 1000000)};
+ nanosleep(&duration, NULL);
+#endif
+}
+
+ScrStr *scr_atomics_wait(ScrBytes *bytes, double index, double expected, double timeout) {
+ size_t i;
+ if (!scr_atomics_index(bytes, index, true, &i)) return NULL;
+ if (!bytes->shared) {
+ static const char message[] = "[object Int32Array] is not a shared typed array.";
+ scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
+ return NULL;
+ }
+ int32_t compare;
+ uint32_t bits = scr_numeric_to_u32(expected);
+ memcpy(&compare, &bits, sizeof compare);
+ scr_shared_enter();
+ if (scr_bytes_get(bytes, (double)i) != compare) {
+ scr_shared_leave();
+ return scr_str_new("not-equal", 9);
+ }
+ double duration = isnan(timeout) ? INFINITY : fmax(timeout, 0);
+ double end = scr_perf_now() + duration;
+ ScrAtomicWaiter waiter = {bytes->shared, (size_t)(bytes->data - bytes->shared->data) + i * 4, false, NULL};
+ ScrAtomicWaiter **tail = &scr_atomic_waiters;
+ while (*tail) tail = &(*tail)->next;
+ *tail = &waiter;
+ bool stopped = false;
+ while (!waiter.notified) {
+ double remaining = end - scr_perf_now();
+ if (!(remaining > 0)) break;
+ /* A bounded wait gives cooperative termination a safe cancellation point.
+ * Registration and condition release share the notify lock: no wakeup can
+ * fall between the expected-value check and entering the wait. */
+ scr_atomics_pause(fmin(remaining, 10));
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+ scr_shared_leave();
+#ifdef SCR_WORKERS
+ stopped = scr_context_checkpoint();
+#endif
+ scr_shared_enter();
+ if (stopped) break;
+#endif
+ }
+ ScrAtomicWaiter **slot = &scr_atomic_waiters;
+ while (*slot != &waiter) slot = &(*slot)->next;
+ *slot = waiter.next;
+ scr_shared_leave();
+ return stopped ? NULL : scr_str_new(waiter.notified ? "ok" : "timed-out", waiter.notified ? 2 : 9);
+}
+
+double scr_atomics_notify(ScrBytes *bytes, double index, double count) {
+ size_t i;
+ if (!scr_atomics_index(bytes, index, true, &i)) return 0;
+ if (!bytes->shared) return 0;
+ double limit = isnan(count) ? 0 : fmax(trunc(count), 0);
+ size_t offset = (size_t)(bytes->data - bytes->shared->data) + i * 4;
+ double notified = 0;
+ scr_shared_enter();
+ for (ScrAtomicWaiter *waiter = scr_atomic_waiters; waiter && notified < limit; waiter = waiter->next) {
+ if (waiter->storage == bytes->shared && waiter->offset == offset && !waiter->notified) {
+ waiter->notified = true;
+ notified++;
+ }
+ }
+#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
+#ifdef _WIN32
+ if (notified) WakeAllConditionVariable(&scr_shared_condition);
+#else
+ if (notified) pthread_cond_broadcast(&scr_shared_condition);
+#endif
+#endif
+ scr_shared_leave();
+ return notified;
+}
diff --git a/packages/runtime/src/scr_shared.test.c b/packages/runtime/src/scr_shared.test.c
new file mode 100644
index 000000000..4de3d3300
--- /dev/null
+++ b/packages/runtime/src/scr_shared.test.c
@@ -0,0 +1,113 @@
+#undef NDEBUG
+#include "scr_runtime.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+enum { THREADS = 4, INCREMENTS = 10000 };
+static atomic_uint ready;
+static atomic_bool cancel_wait;
+typedef struct { ScrSharedBytes *storage; unsigned id; bool waiting; } Task;
+
+static void *run(void *ptr) {
+ Task *task = ptr;
+ scr_context_enter(task->id);
+ scr_init();
+ scr_lib_init(0, NULL);
+ ScrBytes *storage = scr_shared_wrap(task->storage);
+ scr_shared_release(task->storage);
+ ScrBytes *values = scr_bytes_buffer_view(storage, SCR_BYTES_I32, 4, true, 2);
+ ScrBytes *view = scr_dataview_new(storage, 12, true, 4);
+ atomic_fetch_add_explicit(&ready, 1, memory_order_release);
+ if (task->waiting) {
+ scr_context_stop_flag(&cancel_wait);
+ ScrStr *result = scr_atomics_wait(values, 0, 0, INFINITY);
+ if (task->id == 1) {
+ assert(result && result->len == 2 && !memcmp(result->data, "ok", 2));
+ scr_str_release(result);
+ } else assert(!result && scr_context_stopping());
+ } else {
+ while (atomic_load_explicit(&ready, memory_order_acquire) != THREADS) sched_yield();
+ for (unsigned i = 0; i < INCREMENTS; i++) {
+ scr_atomics_op(values, 0, 1, 0, 4);
+ /* These accesses overlap the same shared address with different view
+ * metadata. Non-atomic JS accesses must still be C data-race-free. */
+ scr_dataview_set(view, 0, 0x12345678, SCR_DV_U32, true);
+ assert(scr_dataview_get(view, 0, SCR_DV_U32, true) == 0x12345678);
+ scr_bytes_set(values, 1, -7);
+ assert(scr_bytes_get(values, 1) == -7);
+ if (i % 100 == 0) {
+ ScrBytes *copy = scr_bytes_local_copy(storage);
+ assert(!copy->shared && copy->data != storage->data && copy->len == storage->len);
+ scr_bytes_release(copy);
+ ScrStr *text = scr_bytes_string(storage, 0, storage->len);
+ assert(text->len == storage->len);
+ scr_str_release(text);
+ }
+ }
+ }
+ scr_bytes_release(values);
+ scr_bytes_release(view);
+ scr_bytes_release(storage);
+ scr_context_cleanup();
+ return NULL;
+}
+
+int main(void) {
+ scr_init();
+ scr_lib_init(0, NULL);
+ ScrBytes *storage = scr_bytes_new(SCR_BYTES_U8, 16);
+ scr_bytes_make_shared(storage);
+ ScrBytes *values = scr_bytes_buffer_view(storage, SCR_BYTES_I32, 4, true, 2);
+ scr_bytes_require_unshared(values);
+ assert(scr_exc_pending());
+ scr_exc_clear();
+ ScrBytes *local = scr_bytes_new(SCR_BYTES_U8, 2);
+ ScrBytes *retained = scr_bytes_local_copy(local);
+ assert(retained == local);
+ scr_bytes_require_unshared(local);
+ assert(!scr_exc_pending());
+ scr_bytes_release(retained);
+ scr_bytes_release(local);
+ pthread_t threads[THREADS];
+ Task tasks[THREADS];
+ for (unsigned i = 0; i < THREADS; i++) {
+ tasks[i] = (Task){scr_shared_retain(storage->shared), i + 1, false};
+ assert(!pthread_create(&threads[i], NULL, run, &tasks[i]));
+ }
+ for (unsigned i = 0; i < THREADS; i++) assert(!pthread_join(threads[i], NULL));
+ assert(scr_bytes_get(values, 0) == THREADS * INCREMENTS);
+ assert(scr_atomics_op(values, 0, 3.9, 0, 1) == 3);
+ assert(scr_atomics_op(values, 0, 3, 0xffffffff, 3) == 3);
+ assert(scr_atomics_op(values, 0, 0, 0, 0) == -1);
+ assert(scr_atomics_op(values, 0, 0, 0, 2) == -1);
+ ScrStr *status = scr_atomics_wait(values, 0, 1, INFINITY);
+ assert(status && !strcmp(status->data, "not-equal"));
+ scr_str_release(status);
+ status = scr_atomics_wait(values, 0, 0, 0);
+ assert(status && !strcmp(status->data, "timed-out"));
+ scr_str_release(status);
+
+ tasks[0] = (Task){scr_shared_retain(storage->shared), 1, true};
+ assert(!pthread_create(&threads[0], NULL, run, &tasks[0]));
+ double deadline = scr_perf_now() + 5000;
+ while (scr_atomics_notify(values, 0, 1) == 0) {
+ assert(scr_perf_now() < deadline);
+ sched_yield();
+ }
+ assert(!pthread_join(threads[0], NULL));
+ assert(scr_atomics_notify(values, 0, INFINITY) == 0);
+
+ tasks[0] = (Task){scr_shared_retain(storage->shared), 2, true};
+ assert(!pthread_create(&threads[0], NULL, run, &tasks[0]));
+ atomic_store_explicit(&cancel_wait, true, memory_order_relaxed);
+ assert(!pthread_join(threads[0], NULL));
+ assert(scr_atomics_notify(values, 0, INFINITY) == 0);
+ scr_bytes_release(values);
+ scr_bytes_release(storage);
+ scr_context_cleanup();
+ puts("shared views preserve atomic updates, wait queues and cancellation ownership");
+}
diff --git a/packages/runtime/src/scr_shared.test.ts b/packages/runtime/src/scr_shared.test.ts
new file mode 100644
index 000000000..2b6e8e9f1
--- /dev/null
+++ b/packages/runtime/src/scr_shared.test.ts
@@ -0,0 +1,45 @@
+import { execFile } from "node:child_process";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { promisify } from "node:util";
+import { expect, test } from "vitest";
+import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js";
+
+const exec = promisify(execFile);
+
+test.skipIf(process.platform === "win32")(
+ "shared views serialize concurrent accesses and cancel parked waiters",
+ async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-shared-"));
+ try {
+ const bin = join(dir, "shared");
+ await exec("clang", [
+ "-std=c11",
+ "-O1",
+ "-pthread",
+ "-DSCR_WORKERS",
+ "-DSCR_RC_AUDIT",
+ "-fsanitize=address,undefined",
+ "-Wno-deprecated-declarations",
+ ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
+ ...["scr_shared.test.c", ...EXECUTABLE_RUNTIME_SOURCES].map((name) =>
+ join(import.meta.dirname, name),
+ ),
+ ...(process.platform === "linux" ? ["-lm"] : []),
+ "-o",
+ bin,
+ ]);
+ const result = await exec(bin, [], {
+ timeout: 10000,
+ env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" },
+ });
+ expect(result.stdout).toBe(
+ "shared views preserve atomic updates, wait queues and cancellation ownership\n",
+ );
+ expect(result.stderr).toBe("");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ },
+);
diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c
index 1324fd50a..e00471e81 100644
--- a/packages/runtime/src/scr_stream.c
+++ b/packages/runtime/src/scr_stream.c
@@ -183,11 +183,11 @@ struct ScrStreamState {
* user class whose preorder number happens to collide (scr_stream_is
* below tests interval membership for compiler-emitted subclasses). */
static void scr_stream_release_direct(void *obj);
-ScrVt scr_readable_vt = {1, 0, &scr_stream_release_direct};
-ScrVt scr_writable_vt = {1, 0, &scr_stream_release_direct};
-ScrVt scr_duplex_vt = {1, 0, &scr_stream_release_direct};
-ScrVt scr_transform_vt = {1, 0, &scr_stream_release_direct};
-ScrVt scr_passthrough_vt = {1, 0, &scr_stream_release_direct};
+SCR_TL ScrVt scr_readable_vt = {1, 0, &scr_stream_release_direct};
+SCR_TL ScrVt scr_writable_vt = {1, 0, &scr_stream_release_direct};
+SCR_TL ScrVt scr_duplex_vt = {1, 0, &scr_stream_release_direct};
+SCR_TL ScrVt scr_transform_vt = {1, 0, &scr_stream_release_direct};
+SCR_TL ScrVt scr_passthrough_vt = {1, 0, &scr_stream_release_direct};
static bool scr_vt_within(const ScrVt *vt, const ScrVt *cls) {
return cls->pre <= vt->pre && vt->pre <= cls->post;
@@ -353,8 +353,8 @@ typedef struct ScrStreamTick {
struct ScrStreamTick *next;
} ScrStreamTick;
-static ScrStreamTick *scr_st_head = NULL;
-static ScrStreamTick *scr_st_tail = NULL;
+static SCR_TL ScrStreamTick *scr_st_head = NULL;
+static SCR_TL ScrStreamTick *scr_st_tail = NULL;
static void scr_stream_dispatch_one(void);
@@ -512,7 +512,7 @@ static void *scr_stream_rbuf_take(ScrStreamState *st, size_t n) {
ScrBytes *head = st->r.buf[0];
size_t avail = head->len - st->r.head_off;
size_t take = avail < n - at ? avail : n - at;
- memcpy(out->data + at, head->data + st->r.head_off, take);
+ scr_bytes_read(head, st->r.head_off, out->data + at, take);
at += take;
st->r.head_off += take;
if (st->r.head_off == head->len) {
@@ -2544,7 +2544,7 @@ ScrStream *scr_stream_set_encoding(ScrStream *s, ScrStr *enc) {
ScrBytes *view = chunk;
if (i == 0 && skip > 0) {
view = scr_bytes_new(SCR_BYTES_U8, (double)(chunk->len - skip));
- memcpy(view->data, chunk->data + skip, chunk->len - skip);
+ scr_bytes_read(chunk, skip, view->data, chunk->len - skip);
}
ScrStr *piece = scr_strdec_write(st->r.enc, st->r.dec_pending, view);
st->r.dec_pending = scr_strdec_next(st->r.enc, st->r.dec_pending, view);
diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c
index 79f8cc0b1..776243088 100644
--- a/packages/runtime/src/scr_test.c
+++ b/packages/runtime/src/scr_test.c
@@ -100,19 +100,19 @@ struct ScrTestCtx {
ScrTestHooks hooks[4]; /* suites and root only */
};
-static ScrTestCtx *scr_test_root = NULL;
+static SCR_TL ScrTestCtx *scr_test_root = NULL;
/* Where registrations attach: the suite body being collected or the test
* currently running (its nested test()/t.test() calls are subtests). */
-static ScrTestCtx *scr_test_reg_parent = NULL;
-static bool scr_test_runner_spawned = false;
-static bool scr_test_run_done = false;
-static long scr_test_ct_tests = 0, scr_test_ct_suites = 0, scr_test_ct_pass = 0,
+static SCR_TL ScrTestCtx *scr_test_reg_parent = NULL;
+static SCR_TL bool scr_test_runner_spawned = false;
+static SCR_TL bool scr_test_run_done = false;
+static SCR_TL long scr_test_ct_tests = 0, scr_test_ct_suites = 0, scr_test_ct_pass = 0,
scr_test_ct_fail = 0, scr_test_ct_skipped = 0, scr_test_ct_todo = 0;
-static double scr_test_t0 = 0;
-static bool scr_test_suite_hook_failed = false;
+static SCR_TL double scr_test_t0 = 0;
+static SCR_TL bool scr_test_suite_hook_failed = false;
#ifdef SCR_RC_AUDIT
-static long scr_testctx_live = 0;
+static SCR_TL long scr_testctx_live = 0;
long scr_testctx_live_count(void) { return scr_testctx_live; }
#endif
diff --git a/packages/runtime/src/scr_tls.c b/packages/runtime/src/scr_tls.c
index 14843c5af..2055040f5 100644
--- a/packages/runtime/src/scr_tls.c
+++ b/packages/runtime/src/scr_tls.c
@@ -141,9 +141,9 @@ static ssize_t scr_tls_w32_write(int fd, const void *buf, size_t len) {
/* ── the unit RNG (one CTR_DRBG, lazily seeded) ──────────────────────── */
-static mbedtls_entropy_context scr_tls_entropy;
-static mbedtls_ctr_drbg_context scr_tls_drbg;
-static bool scr_tls_rng_ready = false;
+static SCR_TL mbedtls_entropy_context scr_tls_entropy;
+static SCR_TL mbedtls_ctr_drbg_context scr_tls_drbg;
+static SCR_TL bool scr_tls_rng_ready = false;
/* Defined with the dyn-member hooks at the tail of this file; registered
* from rng_init so every TLS entry point installs the TLSSocket member
@@ -206,7 +206,7 @@ struct ScrSecureCtx {
};
#ifdef SCR_RC_AUDIT
-static long scr_live_secure_ctx = 0;
+static SCR_TL long scr_live_secure_ctx = 0;
long scr_secure_ctx_live_count(void) { return scr_live_secure_ctx; }
#endif
@@ -345,16 +345,16 @@ typedef struct ScrTlsCli {
* first (macOS ships it; Alpine links it), then Debian/Ubuntu's
* ca-certificates.crt, Fedora/RHEL's ca-bundle.crt, and openSUSE's
* ca-bundle.pem. */
-static mbedtls_x509_crt scr_tls_system_roots;
-static bool scr_tls_system_roots_loaded = false;
+static SCR_TL mbedtls_x509_crt scr_tls_system_roots;
+static SCR_TL bool scr_tls_system_roots_loaded = false;
/* setDefaultCACertificates' replacement anchors (scr_tls_ca.c — always
* compiled alongside this unit): parsed lazily per generation, so each
* set re-parses once and every later dial reuses the chain. An EMPTY
* replacement keeps an initialized, certificate-free chain — every
* verification fails, Node's own consequence of trusting nothing. */
-static mbedtls_x509_crt scr_tls_override_roots;
-static uint64_t scr_tls_override_parsed_gen = 0;
+static SCR_TL mbedtls_x509_crt scr_tls_override_roots;
+static SCR_TL uint64_t scr_tls_override_parsed_gen = 0;
#ifdef _WIN32
static void scr_tls_add_windows_ca(void *ctx, const unsigned char *der, size_t len) {
@@ -633,7 +633,7 @@ static void scr_tls_verify_msg(ScrTlsSock *t, char *out, size_t n) {
/* The socket whose handshake this unit is driving — f_sni's route back
* to the answered context (single-threaded runtime; set around
* mbedtls_ssl_handshake). */
-static ScrTlsSock *scr_tls_current = NULL;
+static SCR_TL ScrTlsSock *scr_tls_current = NULL;
/* Defined with the other transport ops below; the answer path needs the
* table's identity to re-find its engine from the boxed socket. */
@@ -1105,7 +1105,7 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat)
memcpy(b->data, v->v.str->data, v->v.str->len);
return b;
}
- if (scr_dyn_bytes_is(v, SCR_BYTES_U8)) return scr_dyn_bytes_unbox(v);
+ if (scr_dyn_bytes_is(v, SCR_BYTES_U8)) return v->v.bytes->shared ? scr_bytes_copy(v->v.bytes) : scr_dyn_bytes_unbox(v);
if (v->kind == SCR_DYN_ARR) {
if (v->v.arr.len == 0) {
ScrJsonBuf b;
@@ -1138,9 +1138,9 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat)
size_t off = 0;
for (size_t i = 0; i < v->v.arr.len; i++) {
const ScrDyn *e = v->v.arr.items[i];
- const char *p = e->kind == SCR_DYN_STR ? e->v.str->data : (const char *)e->v.bytes->data;
size_t n = e->kind == SCR_DYN_STR ? e->v.str->len : e->v.bytes->len;
- memcpy(b->data + off, p, n);
+ if (e->kind == SCR_DYN_STR) memcpy(b->data + off, e->v.str->data, n);
+ else scr_bytes_read(e->v.bytes, 0, b->data + off, n);
off += n;
b->data[off++] = '\n'; /* PEM blocks join on line boundaries */
}
diff --git a/packages/runtime/src/scr_tls_ca.c b/packages/runtime/src/scr_tls_ca.c
index 8d23a10c2..ce8a92ecb 100644
--- a/packages/runtime/src/scr_tls_ca.c
+++ b/packages/runtime/src/scr_tls_ca.c
@@ -33,16 +33,23 @@
/* The per-type caches: +1 held here for the process lifetime (an atexit
* teardown releases them so the RC audit stays clean). 'default' also
* keeps the concatenated PEM buffer scr_tls.c parses into anchors. */
-static ScrArr *scr_ca_bundled = NULL;
-static ScrArr *scr_ca_system = NULL;
-static ScrArr *scr_ca_extra = NULL;
-static ScrArr *scr_ca_default = NULL;
-static char *scr_ca_extra_buf = NULL;
-static size_t scr_ca_extra_len = 0;
-static bool scr_ca_installed = false;
-static char *scr_ca_override_buf = NULL;
-static size_t scr_ca_override_len = 0;
-static uint64_t scr_ca_override_gen = 0; /* 0 = never set */
+static SCR_TL ScrArr *scr_ca_bundled = NULL;
+static SCR_TL ScrArr *scr_ca_system = NULL;
+static SCR_TL ScrArr *scr_ca_extra = NULL;
+static SCR_TL ScrArr *scr_ca_default = NULL;
+static SCR_TL char *scr_ca_extra_buf = NULL;
+static SCR_TL size_t scr_ca_extra_len = 0;
+static SCR_TL bool scr_ca_installed = false;
+static SCR_TL char *scr_ca_override_buf = NULL;
+static SCR_TL size_t scr_ca_override_len = 0;
+static SCR_TL uint64_t scr_ca_override_gen = 0; /* 0 = never set */
+
+#ifdef SCR_WORKERS
+/* Published during executable startup, before any worker exists. The main
+ * context owns these raw bytes until all its children have been joined. */
+static const char *scr_ca_initial_extra;
+static size_t scr_ca_initial_extra_len;
+#endif
static void scr_ca_oom(void) {
fputs("scriptc: out of memory\n", stderr);
@@ -62,12 +69,12 @@ static void scr_ca_teardown(void) {
scr_ca_override_buf = NULL;
}
-static bool scr_ca_teardown_armed = false;
+static SCR_TL bool scr_ca_teardown_armed = false;
static void scr_ca_arm_teardown(void) {
if (!scr_ca_teardown_armed) {
scr_ca_teardown_armed = true;
- atexit(scr_ca_teardown);
+ scr_atexit(scr_ca_teardown);
}
}
@@ -145,10 +152,25 @@ void scr_tls_ca_install(void) {
if (scr_ca_installed) return;
scr_ca_installed = true;
scr_ca_arm_teardown();
+#ifdef SCR_WORKERS
+ if (!scr_context_is_main()) {
+ if (scr_ca_initial_extra) {
+ scr_ca_extra_len = scr_ca_initial_extra_len;
+ scr_ca_extra_buf = malloc(scr_ca_extra_len + 1);
+ if (!scr_ca_extra_buf) scr_ca_oom();
+ memcpy(scr_ca_extra_buf, scr_ca_initial_extra, scr_ca_extra_len + 1);
+ }
+ return;
+ }
+#endif
const char *path = getenv("NODE_EXTRA_CA_CERTS");
if (path != NULL && path[0] != '\0') {
scr_ca_extra_buf = scr_ca_read_file(path, &scr_ca_extra_len);
}
+#ifdef SCR_WORKERS
+ scr_ca_initial_extra = scr_ca_extra_buf;
+ scr_ca_initial_extra_len = scr_ca_extra_len;
+#endif
}
bool scr_tls_ca_extra_pem(const char **pem, size_t *len) {
diff --git a/packages/runtime/src/scr_util_compare.c b/packages/runtime/src/scr_util_compare.c
index 4f88886ee..0a9dbcddd 100644
--- a/packages/runtime/src/scr_util_compare.c
+++ b/packages/runtime/src/scr_util_compare.c
@@ -184,6 +184,7 @@ static bool util_deep_equal(const ScrDyn *a, const ScrDyn *b, bool skip,
if (a->kind == SCR_DYN_BYTES) {
const ScrBytes *x = a->v.bytes, *y = b->v.bytes;
if (x->elem != y->elem || x->len != y->len) return false;
+ SCR_SHARED_GUARD(x, y);
size_t length = x->len * scr_bytes_elem_size(x->elem);
if (length && memcmp(x->data, y->data, length)) return false;
}
diff --git a/packages/runtime/src/scr_util_style.c b/packages/runtime/src/scr_util_style.c
index acfd689e1..8d12d94c5 100644
--- a/packages/runtime/src/scr_util_style.c
+++ b/packages/runtime/src/scr_util_style.c
@@ -148,7 +148,7 @@ static ScrStr *util_style_replace(const ScrStr *text, const char *close,
}
static bool util_style_env_nonempty(const char *name) {
- const char *value = getenv(name);
+ const char *value = scr_getenv(name);
return value && *value;
}
@@ -174,7 +174,7 @@ done:
/* styleText only needs the color/no-color distinction, rather than the
* full 4/8/24-bit depth. Follow the ordering of Node 24 internal/tty. */
static bool util_style_terminal_colors(void) {
- const char *force = getenv("FORCE_COLOR");
+ const char *force = scr_getenv("FORCE_COLOR");
if (force) {
bool enabled = !strcmp(force, "") || !strcmp(force, "true") ||
!strcmp(force, "1") || !strcmp(force, "2") || !strcmp(force, "3");
@@ -192,21 +192,21 @@ static bool util_style_terminal_colors(void) {
}
return enabled;
}
- const char *term = getenv("TERM");
+ const char *term = scr_getenv("TERM");
if (util_style_env_nonempty("NO_COLOR") || util_style_env_nonempty("NODE_DISABLE_COLORS") ||
(term && !strcmp(term, "dumb"))) return false;
#ifdef _WIN32
return true;
#endif
if (util_style_env_nonempty("TMUX")) return true;
- if (getenv("TF_BUILD") && getenv("AGENT_NAME")) return true;
- if (getenv("CI")) {
+ if (scr_getenv("TF_BUILD") && scr_getenv("AGENT_NAME")) return true;
+ if (scr_getenv("CI")) {
static const char *const ci[] = { "APPVEYOR", "BUILDKITE", "CIRCLECI", "DRONE", "GITEA_ACTIONS", "GITHUB_ACTIONS", "GITLAB_CI", "TRAVIS" };
- for (size_t i = 0; i < sizeof ci / sizeof ci[0]; i++) if (getenv(ci[i])) return true;
- const char *name = getenv("CI_NAME");
+ for (size_t i = 0; i < sizeof ci / sizeof ci[0]; i++) if (scr_getenv(ci[i])) return true;
+ const char *name = scr_getenv("CI_NAME");
return name && !strcmp(name, "codeship");
}
- const char *teamcity = getenv("TEAMCITY_VERSION");
+ const char *teamcity = scr_getenv("TEAMCITY_VERSION");
if (teamcity) {
const char *p = teamcity;
if (!strncmp(p, "9.", 2)) {
@@ -218,10 +218,10 @@ static bool util_style_terminal_colors(void) {
while (*p >= '0' && *p <= '9') p++;
return p - teamcity >= 2 && *p == '.';
}
- const char *program = getenv("TERM_PROGRAM");
+ const char *program = scr_getenv("TERM_PROGRAM");
if (program && (!strcmp(program, "iTerm.app") || !strcmp(program, "HyperTerm") ||
!strcmp(program, "MacTerm") || !strcmp(program, "Apple_Terminal"))) return true;
- const char *colorterm = getenv("COLORTERM");
+ const char *colorterm = scr_getenv("COLORTERM");
if (colorterm && (!strcmp(colorterm, "truecolor") || !strcmp(colorterm, "24bit"))) return true;
if (term) {
if (strstr(term, "truecolor") || !strncmp(term, "xterm-256", 9)) return true;
@@ -294,7 +294,7 @@ ScrStr *scr_util_style_text(const ScrDyn *format, const ScrDyn *text, const ScrD
scr_dyn_arg_type_fail("stream", "an instance of ReadableStream, WritableStream, or Stream", stream); goto done;
}
}
- colorize = getenv("FORCE_COLOR") ? util_style_terminal_colors() :
+ colorize = scr_getenv("FORCE_COLOR") ? util_style_terminal_colors() :
scr_process_is_tty(fd) && util_style_terminal_colors();
}
}
diff --git a/packages/runtime/src/scr_watch.c b/packages/runtime/src/scr_watch.c
index 9d828a0b1..0d299d943 100644
--- a/packages/runtime/src/scr_watch.c
+++ b/packages/runtime/src/scr_watch.c
@@ -121,11 +121,11 @@ struct ScrWatcher {
struct ScrWatcher *next; /* the open-watcher registry (+1 each) */
};
-static ScrWatcher *scr_watchers = NULL;
-static size_t scr_watchers_open = 0; /* liveness: open (unclosed) watchers */
+static SCR_TL ScrWatcher *scr_watchers = NULL;
+static SCR_TL size_t scr_watchers_open = 0; /* liveness: open (unclosed) watchers */
#ifdef SCR_HAVE_KQUEUE
-static int scr_watch_kq = -1; /* created with the first watch; lives forever */
+static SCR_TL int scr_watch_kq = -1; /* created with the first watch; lives forever */
/* Arms the vnode filter for a watcher's current fd. EV_CLEAR: state resets
* at delivery, so an unserviced event never busy-spins the idle poll (the
@@ -145,7 +145,7 @@ static bool scr_watch_arm(ScrWatcher *w) {
#endif
#ifdef SCR_HAVE_INOTIFY
-static int scr_watch_ino = -1; /* created with the first watch; lives forever */
+static SCR_TL int scr_watch_ino = -1; /* created with the first watch */
/* libuv's subscription mask: name-level events for directory watches,
* self events for the file case, IN_IGNORED arriving for free. */
@@ -557,12 +557,22 @@ static void scr_watch_cleanup_atexit(void) {
scr_watcher_drop_listeners(w);
scr_watcher_release(w);
}
+#ifdef SCR_WORKERS
+#ifdef SCR_HAVE_KQUEUE
+ if (scr_watch_kq >= 0) close(scr_watch_kq);
+ scr_watch_kq = -1;
+#endif
+#ifdef SCR_HAVE_INOTIFY
+ if (scr_watch_ino >= 0) close(scr_watch_ino);
+ scr_watch_ino = -1;
+#endif
+#endif
}
void scr_watch_install(void) {
- static bool installed = false;
+ static SCR_TL bool installed = false;
if (installed) return;
installed = true;
- atexit(scr_watch_cleanup_atexit);
+ scr_atexit(scr_watch_cleanup_atexit);
scr_loop_set_watch(&scr_watch_pending, &scr_watch_dispatch, &scr_watch_pollfd);
}
diff --git a/packages/runtime/src/scr_worker.c b/packages/runtime/src/scr_worker.c
new file mode 100644
index 000000000..0fc986a8e
--- /dev/null
+++ b/packages/runtime/src/scr_worker.c
@@ -0,0 +1,232 @@
+#include "scr_worker.h"
+
+#ifdef SCR_WORKERS
+#include
+#include
+#include
+#ifdef _WIN32
+#include
+#include
+#else
+#include
+#endif
+
+struct ScrWorkerThread {
+ atomic_size_t references;
+ atomic_bool cancel;
+ atomic_bool port_closed;
+ uint64_t id;
+ uint32_t root;
+ ScrWorkerEntry entry;
+ ScrMailbox *inbox;
+ ScrMailbox *parent;
+ ScrMessage *data;
+ ScrContextEnv *environment;
+ int argc;
+ char **argv;
+ bool joined;
+#ifdef _WIN32
+ HANDLE thread;
+#else
+ pthread_t thread;
+#endif
+};
+
+static atomic_uint_fast64_t scr_worker_next_id = 1;
+static SCR_TL ScrWorkerThread *scr_worker_current;
+static SCR_TL ScrDyn *scr_worker_local_data;
+
+static ScrMailEvent *scr_worker_event(unsigned kind, uint64_t source,
+ int code, ScrMessage *message) {
+ ScrMailEvent *event = calloc(1, sizeof(*event));
+ if (!event) scr_trap("scriptc: out of memory\n");
+ *event = (ScrMailEvent){.kind = kind, .source = source, .code = code, .message = message};
+ return event;
+}
+
+ScrWorkerThread *scr_worker_thread_retain(ScrWorkerThread *worker) {
+ atomic_fetch_add_explicit(&worker->references, 1, memory_order_relaxed);
+ return worker;
+}
+
+void scr_worker_thread_release(ScrWorkerThread *worker) {
+ if (!worker || atomic_fetch_sub_explicit(&worker->references, 1, memory_order_acq_rel) != 1) return;
+#ifdef _WIN32
+ if (worker->thread) CloseHandle(worker->thread);
+#else
+ if (!worker->joined) (void)pthread_detach(worker->thread);
+#endif
+ scr_mailbox_release(worker->inbox);
+ scr_mailbox_release(worker->parent);
+ scr_message_free(worker->data);
+ scr_context_env_free(worker->environment);
+ for (int i = 0; i < worker->argc; i++) free(worker->argv[i]);
+ free(worker->argv);
+ free(worker);
+}
+
+static void scr_worker_data_cleanup(void) {
+ ScrDyn *data = scr_worker_local_data;
+ scr_worker_local_data = NULL;
+ scr_dyn_release(data);
+}
+
+ScrDyn *scr_worker_data(void) {
+ if (!scr_worker_current) return scr_dyn_new_null();
+ if (!scr_worker_local_data) {
+ ScrDyn *data = scr_message_decode(scr_worker_current->data);
+ if (!data) return NULL;
+ scr_worker_local_data = data;
+ if (scr_context_atexit(scr_worker_data_cleanup) != 0) scr_trap("scriptc: out of memory\n");
+ scr_message_free(scr_worker_current->data);
+ scr_worker_current->data = NULL;
+ }
+ return scr_dyn_retain(scr_worker_local_data);
+}
+
+ScrMailbox *scr_worker_inbox(void) {
+ return scr_worker_current ? scr_worker_current->inbox : NULL;
+}
+
+double scr_worker_root(void) { return scr_worker_current ? (double)scr_worker_current->root : -1; }
+int scr_worker_argc(void) { return scr_worker_current ? scr_worker_current->argc : 0; }
+char **scr_worker_argv(void) { return scr_worker_current ? scr_worker_current->argv : NULL; }
+
+uint64_t scr_worker_thread_id(const ScrWorkerThread *worker) { return worker->id; }
+
+void scr_worker_thread_terminate(ScrWorkerThread *worker) {
+ atomic_store_explicit(&worker->cancel, true, memory_order_relaxed);
+ scr_mailbox_post(worker->inbox, scr_worker_event(SCR_WORKER_WAKE, 0, 0, NULL));
+}
+
+bool scr_worker_thread_post(ScrWorkerThread *worker, ScrMessage *message) {
+ if (atomic_load_explicit(&worker->port_closed, memory_order_relaxed)) {
+ scr_message_free(message);
+ return false;
+ }
+ return scr_mailbox_post(worker->inbox, scr_worker_event(SCR_WORKER_MESSAGE, 0, 0, message));
+}
+
+void scr_worker_port_close(void) {
+ if (scr_worker_current)
+ atomic_store_explicit(&scr_worker_current->port_closed, true, memory_order_relaxed);
+}
+
+bool scr_worker_post_parent(ScrMessage *message) {
+ if (!scr_worker_current) { scr_message_free(message); return false; }
+ return scr_mailbox_post(scr_worker_current->parent,
+ scr_worker_event(SCR_WORKER_MESSAGE, scr_worker_current->id, 0, message));
+}
+
+static void scr_worker_report_error(void) {
+ ScrCaught *caught = scr_exc_take();
+ ScrDyn *value = scr_caught_to_dyn(caught);
+ ScrMessage *message = scr_message_encode(value);
+ scr_dyn_release(value);
+ scr_caught_release(caught);
+ if (!message) {
+ scr_exc_clear();
+ static const char text[] = "Serializing an uncaught exception failed";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, text, sizeof text - 1, "ERR_WORKER_UNSERIALIZABLE_ERROR");
+ caught = scr_exc_take();
+ value = scr_caught_to_dyn(caught);
+ message = scr_message_encode(value);
+ scr_dyn_release(value);
+ scr_caught_release(caught);
+ }
+ scr_mailbox_post(scr_worker_current->parent,
+ scr_worker_event(SCR_WORKER_ERROR, scr_worker_current->id, 0, message));
+}
+
+static void scr_worker_thread_run(ScrWorkerThread *worker) {
+ scr_context_enter(worker->id);
+ scr_context_stop_flag(&worker->cancel);
+ scr_context_env_enter(worker->environment);
+ worker->environment = NULL;
+ scr_worker_current = worker;
+ scr_context_report_error = scr_worker_report_error;
+ scr_mailbox_post(worker->parent, scr_worker_event(SCR_WORKER_ONLINE, worker->id, 0, NULL));
+ int code = worker->entry(worker->root);
+ scr_mailbox_close(worker->inbox);
+ scr_context_cleanup();
+ scr_worker_current = NULL;
+ /* Cleanup may itself finish owner-bound native operations. Publish exit
+ * only once none of those operations can touch the worker's script heap. */
+ scr_mailbox_post(worker->parent, scr_worker_event(SCR_WORKER_EXIT, worker->id, code, NULL));
+ scr_worker_thread_release(worker);
+}
+
+#ifdef _WIN32
+static unsigned __stdcall scr_worker_thread_main(void *data) {
+ scr_worker_thread_run(data);
+ return 0;
+}
+#else
+static void *scr_worker_thread_main(void *data) {
+ scr_worker_thread_run(data);
+ return NULL;
+}
+#endif
+
+ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root,
+ ScrMessage *data, ScrMailbox *parent,
+ ScrArr *arguments) {
+ ScrWorkerThread *worker = calloc(1, sizeof(*worker));
+ if (!worker) scr_trap("scriptc: out of memory\n");
+ atomic_init(&worker->references, 2); /* owner and native thread */
+ atomic_init(&worker->cancel, false);
+ atomic_init(&worker->port_closed, false);
+ worker->id = atomic_fetch_add_explicit(&scr_worker_next_id, 1, memory_order_relaxed);
+ worker->entry = entry;
+ worker->root = root;
+ worker->data = data;
+ worker->environment = scr_context_env_capture();
+ worker->parent = scr_mailbox_retain(parent);
+ if (arguments) {
+ if (arguments->len > INT_MAX) scr_trap("scriptc: too many worker arguments\n");
+ worker->argc = (int)arguments->len;
+ worker->argv = calloc(arguments->len + 1, sizeof(*worker->argv));
+ if (!worker->argv) scr_trap("scriptc: out of memory\n");
+ for (size_t i = 0; i < arguments->len; i++) {
+ ScrStr *text = scr_arr_get_ref(arguments, (double)i);
+ worker->argv[i] = malloc(text->len + 1);
+ if (!worker->argv[i]) scr_trap("scriptc: out of memory\n");
+ memcpy(worker->argv[i], text->data, text->len + 1);
+ scr_str_release(text);
+ }
+ }
+ worker->inbox = scr_mailbox_new();
+ if (!worker->inbox) goto failed;
+#ifdef _WIN32
+ worker->thread = (HANDLE)_beginthreadex(NULL, 0, scr_worker_thread_main, worker, 0, NULL);
+ if (!worker->thread) goto failed;
+#else
+ if (pthread_create(&worker->thread, NULL, scr_worker_thread_main, worker) != 0) goto failed;
+#endif
+ return worker;
+failed:
+ for (int i = 0; i < worker->argc; i++) free(worker->argv[i]);
+ free(worker->argv);
+ scr_context_env_free(worker->environment);
+ scr_mailbox_release(worker->inbox);
+ scr_mailbox_release(worker->parent);
+ scr_message_free(worker->data);
+ free(worker);
+ if (!scr_exc_pending()) {
+ static const char message[] = "Could not start worker thread";
+ scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "ERR_WORKER_INIT_FAILED");
+ }
+ return NULL;
+}
+
+void scr_worker_thread_join(ScrWorkerThread *worker) {
+ if (worker->joined) return;
+#ifdef _WIN32
+ if (WaitForSingleObject(worker->thread, INFINITE) != WAIT_OBJECT_0)
+ scr_trap("scriptc: worker join failed\n");
+#else
+ if (pthread_join(worker->thread, NULL) != 0) scr_trap("scriptc: worker join failed\n");
+#endif
+ worker->joined = true;
+}
+#endif
diff --git a/packages/runtime/src/scr_worker.h b/packages/runtime/src/scr_worker.h
new file mode 100644
index 000000000..cc00a8f02
--- /dev/null
+++ b/packages/runtime/src/scr_worker.h
@@ -0,0 +1,42 @@
+#ifndef SCR_WORKER_H
+#define SCR_WORKER_H
+
+#include "scr_mailbox.h"
+
+/* A thread carries only neutral transport state and an immutable compiled
+ * entry point. Script-visible handles and listeners stay in their owner's
+ * runtime heap. The entry initializes that thread's runtime before user code;
+ * the thread wrapper drains its context before publishing the exit event. */
+typedef struct ScrWorkerThread ScrWorkerThread;
+typedef int (*ScrWorkerEntry)(uint32_t root);
+
+enum {
+ SCR_WORKER_ONLINE = 1,
+ SCR_WORKER_MESSAGE,
+ SCR_WORKER_ERROR,
+ SCR_WORKER_EXIT,
+ SCR_WORKER_WAKE,
+ SCR_WORKER_STDOUT,
+ SCR_WORKER_STDERR,
+};
+
+/* Consumes data, including on failure. Borrows the parent's mailbox. */
+ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root,
+ ScrMessage *data, ScrMailbox *parent,
+ ScrArr *arguments);
+ScrWorkerThread *scr_worker_thread_retain(ScrWorkerThread *worker);
+void scr_worker_thread_release(ScrWorkerThread *worker);
+/* Join is owner-only and idempotent. The caller holds a reference. */
+void scr_worker_thread_join(ScrWorkerThread *worker);
+void scr_worker_thread_terminate(ScrWorkerThread *worker);
+uint64_t scr_worker_thread_id(const ScrWorkerThread *worker);
+/* Posting consumes the message even after the destination has closed. */
+bool scr_worker_thread_post(ScrWorkerThread *worker, ScrMessage *message);
+bool scr_worker_post_parent(ScrMessage *message);
+void scr_worker_port_close(void);
+/* Current-thread access: inbox is borrowed, data is a retained local value. */
+ScrMailbox *scr_worker_inbox(void);
+void scr_loop_set_workers(bool (*pending)(void), bool (*ready)(void), void (*dispatch)(void),
+ int (*pollfd)(void), void (*wait)(double));
+
+#endif
diff --git a/packages/runtime/src/scr_worker.test.c b/packages/runtime/src/scr_worker.test.c
new file mode 100644
index 000000000..f8ecee48a
--- /dev/null
+++ b/packages/runtime/src/scr_worker.test.c
@@ -0,0 +1,115 @@
+#undef NDEBUG
+#include "scr_worker.h"
+#include
+#include
+#include
+#include
+
+enum { WORKERS = 4, MESSAGES = 16 };
+static atomic_bool start;
+static SCR_TL unsigned timer_calls;
+static SCR_TL unsigned cleanup_calls;
+
+static void check_cleanup(void) { cleanup_calls++; }
+
+static void timer(ScrClosure *closure) {
+ (void)closure;
+ assert(!scr_context_is_main());
+ timer_calls++;
+}
+
+static int entry(uint32_t root_id) {
+ scr_init();
+ scr_lib_init(0, NULL);
+ assert(!scr_context_is_main());
+ assert(timer_calls == 0 && cleanup_calls == 0);
+ assert(scr_context_atexit(check_cleanup) == 0);
+ while (!atomic_load_explicit(&start, memory_order_acquire)) sched_yield();
+ ScrDyn *input = scr_worker_data();
+ assert(input && !scr_exc_pending());
+ assert(scr_dyn_obj_get(input, "root", 4)->v.num == root_id);
+ assert(scr_dyn_obj_get(input, "self", 4) == input);
+ ScrDyn *again = scr_worker_data();
+ assert(input == again);
+ scr_dyn_release(again);
+ scr_dyn_release(input);
+
+ for (unsigned i = 0; i < MESSAGES; i++) {
+ ScrDyn *value = scr_dyn_new_num(root_id * 100 + i);
+ ScrMessage *wire = scr_message_encode(value);
+ scr_dyn_release(value);
+ assert(wire && scr_worker_post_parent(wire));
+ ScrMailEvent *reply = NULL;
+ while (!(reply = scr_mailbox_take(scr_worker_inbox())))
+ scr_mailbox_wait(scr_worker_inbox(), 1000);
+ assert(reply->kind == SCR_WORKER_MESSAGE && reply->source == 0);
+ value = scr_message_decode(reply->message);
+ assert(value && value->kind == SCR_DYN_NUM && value->v.num == root_id * 100 + i);
+ scr_dyn_release(value);
+ scr_mail_event_free(reply);
+ }
+ scr_set_timeout(scr_closure_new((void *)timer, 0), 1);
+ assert(!scr_loop_run(NULL) && !scr_exc_pending());
+ assert(timer_calls == 1 && cleanup_calls == 0);
+ return (int)root_id;
+}
+
+int main(void) {
+ scr_init();
+ scr_lib_init(0, NULL);
+ ScrMailbox *mailbox = scr_mailbox_new();
+ assert(mailbox);
+ ScrWorkerThread *workers[WORKERS];
+ unsigned messages[WORKERS] = {0};
+ bool online[WORKERS] = {false};
+ bool exited[WORKERS] = {false};
+ for (unsigned i = 0; i < WORKERS; i++) {
+ ScrDyn *input = scr_dyn_new_obj();
+ scr_dyn_obj_set(input, "root", 4, scr_dyn_new_num(i));
+ scr_dyn_obj_set(input, "self", 4, scr_dyn_retain(input));
+ ScrMessage *wire = scr_message_encode(input);
+ scr_dyn_release(input);
+ assert(wire && !scr_exc_pending());
+ workers[i] = scr_worker_thread_start(entry, i, wire, mailbox, NULL);
+ assert(workers[i] && !scr_exc_pending());
+ assert(scr_worker_thread_id(workers[i]) != 0);
+ }
+ /* No sender heap survives into the workerData decode. */
+ scr_collect_cycles();
+ atomic_store_explicit(&start, true, memory_order_release);
+ unsigned exit_count = 0;
+ while (exit_count != WORKERS) {
+ ScrMailEvent *event = scr_mailbox_take(mailbox);
+ if (!event) { scr_mailbox_wait(mailbox, 1000); continue; }
+ unsigned i = 0;
+ while (i < WORKERS && scr_worker_thread_id(workers[i]) != event->source) i++;
+ assert(i < WORKERS && !exited[i]);
+ if (event->kind == SCR_WORKER_ONLINE) {
+ assert(!online[i]);
+ online[i] = true;
+ } else if (event->kind == SCR_WORKER_MESSAGE) {
+ assert(online[i] && messages[i] < MESSAGES);
+ ScrDyn *value = scr_message_decode(event->message);
+ assert(value && value->kind == SCR_DYN_NUM && value->v.num == i * 100 + messages[i]);
+ messages[i]++;
+ ScrMessage *reply = scr_message_encode(value);
+ scr_dyn_release(value);
+ assert(reply && scr_worker_thread_post(workers[i], reply));
+ } else {
+ assert(event->kind == SCR_WORKER_EXIT && event->code == (int)i);
+ assert(online[i] && messages[i] == MESSAGES);
+ exited[i] = true;
+ exit_count++;
+ scr_worker_thread_join(workers[i]);
+ scr_worker_thread_join(workers[i]);
+ ScrMessage *late = scr_message_encode(scr_dyn_undefined());
+ assert(late && !scr_worker_thread_post(workers[i], late));
+ }
+ scr_mail_event_free(event);
+ }
+ for (unsigned i = 0; i < WORKERS; i++) scr_worker_thread_release(workers[i]);
+ scr_mailbox_release(mailbox);
+ assert(timer_calls == 0 && cleanup_calls == 0 && scr_context_is_main());
+ scr_context_cleanup();
+ puts("worker threads isolate heaps, exchange messages and publish ordered exits");
+}
diff --git a/packages/runtime/src/scr_worker.test.ts b/packages/runtime/src/scr_worker.test.ts
new file mode 100644
index 000000000..edcca4eaa
--- /dev/null
+++ b/packages/runtime/src/scr_worker.test.ts
@@ -0,0 +1,49 @@
+import { execFile } from "node:child_process";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { promisify } from "node:util";
+import { expect, test } from "vitest";
+import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-toolchain.js";
+
+const exec = promisify(execFile);
+
+test.skipIf(process.platform === "win32")(
+ "native workers own their data and finish all message exchanges before exit",
+ async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-workers-"));
+ try {
+ const bin = join(dir, "workers");
+ await exec("clang", [
+ "-std=c11",
+ "-O1",
+ "-pthread",
+ "-DSCR_WORKERS",
+ "-DSCR_RC_AUDIT",
+ "-fsanitize=address,undefined",
+ "-Wno-deprecated-declarations",
+ ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
+ ...[
+ "scr_worker.test.c",
+ "scr_worker.c",
+ "scr_mailbox.c",
+ "scr_message.c",
+ ...EXECUTABLE_RUNTIME_SOURCES,
+ ].map((name) => join(import.meta.dirname, name)),
+ ...(process.platform === "linux" ? ["-lm"] : []),
+ "-o",
+ bin,
+ ]);
+ const result = await exec(bin, [], {
+ timeout: 10000,
+ env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" },
+ });
+ expect(result.stdout).toBe(
+ "worker threads isolate heaps, exchange messages and publish ordered exits\n",
+ );
+ expect(result.stderr).toBe("");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ },
+);
diff --git a/packages/runtime/src/scr_worker_events.c b/packages/runtime/src/scr_worker_events.c
new file mode 100644
index 000000000..ca788e54a
--- /dev/null
+++ b/packages/runtime/src/scr_worker_events.c
@@ -0,0 +1,443 @@
+#include "scr_worker.h"
+
+#ifdef SCR_WORKERS
+#include
+
+extern int sc_worker_entry(uint32_t root);
+
+typedef struct ScrWorkerView {
+ size_t rc;
+ ScrEmitter *events;
+ ScrWorkerThread *thread;
+ ScrPromise *termination;
+ bool port;
+ bool closed;
+ bool referenced;
+ bool started;
+ bool closing;
+ ScrMailEvent *queued_head;
+ ScrMailEvent *queued_tail;
+ struct ScrWorkerView *next;
+} ScrWorkerView;
+
+static SCR_TL ScrMailbox *scr_worker_events;
+static SCR_TL ScrWorkerView *scr_worker_views;
+static SCR_TL ScrWorkerView *scr_worker_port;
+static const char *const scr_worker_method_names[] = {
+ "ref", "unref", "hasRef", "terminate", "postMessage", "on", "once", "addListener",
+ "prependListener", "prependOnceListener", "off", "removeListener", "removeAllListeners",
+ "listenerCount", "close", "start",
+};
+static SCR_TL ScrDyn *scr_worker_methods[sizeof scr_worker_method_names / sizeof scr_worker_method_names[0]];
+
+static void scr_worker_view_trace(void *ptr, ScrTraceVisit visit, void *ctx) {
+ visit(((ScrWorkerView *)ptr)->events, ctx);
+ visit(((ScrWorkerView *)ptr)->termination, ctx);
+}
+
+static void scr_worker_view_gcfree(void *ptr) {
+ ScrWorkerView *view = ptr;
+ scr_worker_thread_release(view->thread);
+ while (view->queued_head) {
+ ScrMailEvent *event = view->queued_head;
+ view->queued_head = event->next;
+ scr_mail_event_free(event);
+ }
+ scr_obj_free_note();
+ scr_cyc_free(ptr);
+}
+
+static void scr_worker_view_destroy(void *ptr) {
+ ScrWorkerView *view = ptr;
+ scr_emitter_release(view->events);
+ scr_promise_release(view->termination);
+ scr_worker_view_gcfree(ptr);
+}
+
+static void *scr_worker_view_retain(void *ptr) {
+ ScrWorkerView *view = ptr;
+ view->rc++;
+ scr_cyc_mark_live(view);
+ return view;
+}
+
+static void scr_worker_view_release(void *ptr) {
+ ScrWorkerView *view = ptr;
+ if (!view) return;
+ if (--view->rc == 0) {
+ scr_cyc_on_dead(view);
+ scr_rc_destroy(view, scr_worker_view_destroy);
+ } else scr_cyc_on_release(view);
+}
+
+static void scr_worker_port_listeners(void *ptr, const ScrStr *name, bool present) {
+ ScrWorkerView *view = ptr;
+ if (name->len != 7 || memcmp(name->data, "message", 7)) return;
+ if (present) view->started = true;
+ view->referenced = present;
+}
+
+static ScrWorkerView *scr_worker_view_new(bool port) {
+ ScrWorkerView *view = scr_cyc_alloc(sizeof(*view), scr_worker_view_trace, scr_worker_view_gcfree);
+ view->rc = 1;
+ view->port = port;
+ view->referenced = !port;
+ view->events = scr_emitter_new();
+ view->events->cls = port ? "MessagePort" : "Worker";
+ if (port) scr_emitter_observe(view->events, scr_worker_port_listeners, view);
+ scr_obj_alloc_note();
+ return view;
+}
+
+static ScrDyn *scr_worker_refusal(const char *operation) {
+ ScrStr *message = scr_str_new(operation, strlen(operation));
+ scr_throw_lowering_fence(message);
+ scr_str_release(message);
+ return NULL;
+}
+
+static bool scr_worker_empty_transfer(const ScrDyn *value) {
+ if (value->kind == SCR_DYN_TYPED_REF) {
+ ScrDyn *view = scr_dyn_typed_ref_materialize(value);
+ bool empty = view && !scr_exc_pending() && scr_worker_empty_transfer(view);
+ scr_dyn_release(view);
+ return empty;
+ }
+ return value->kind == SCR_DYN_UNDEF || (value->kind == SCR_DYN_ARR && value->v.arr.len == 0);
+}
+
+static ScrDyn *scr_worker_view_invoke(void *ptr, ScrDyn *self, const char *method,
+ ScrDyn *const *args, size_t argc, const char *what) {
+ (void)what;
+ ScrWorkerView *view = ptr;
+ if (!strcmp(method, "ref") || !strcmp(method, "unref")) {
+ view->referenced = !strcmp(method, "ref");
+ return scr_dyn_retain(scr_dyn_undefined());
+ }
+ if (view->port && !strcmp(method, "hasRef"))
+ return scr_dyn_new_bool(view->referenced && !view->closed);
+ if (!view->port && !strcmp(method, "terminate")) {
+ if (!view->termination) {
+ view->termination = scr_promise_new();
+ if (view->closed) scr_promise_fulfill_ref(view->termination,
+ scr_dyn_retain(scr_dyn_undefined()), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
+ else scr_worker_thread_terminate(view->thread);
+ }
+ return scr_dyn_new_promise(view->termination);
+ }
+ if (!strcmp(method, "postMessage")) {
+ /* Worker drops messages after exit before validating or cloning. A
+ * closed MessagePort still validates its payload, as Node does. */
+ if (!view->port && view->closed) return scr_dyn_retain(scr_dyn_undefined());
+ if (!argc) {
+ static const char message[] = "The \"value\" argument must be specified";
+ scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS");
+ return NULL;
+ }
+ if (argc > 1 && !scr_worker_empty_transfer(args[1]))
+ return scr_exc_pending() ? NULL : scr_worker_refusal("Worker postMessage transfer lists are not supported yet");
+ ScrMessage *message = scr_message_encode(args[0]);
+ if (!message) return NULL;
+ if (view->closed) scr_message_free(message);
+ else if (view->port) scr_worker_post_parent(message);
+ else scr_worker_thread_post(view->thread, message);
+ return scr_dyn_retain(scr_dyn_undefined());
+ }
+ bool once = !strcmp(method, "once") || !strcmp(method, "prependOnceListener");
+ bool prepend = !strcmp(method, "prependListener") || !strcmp(method, "prependOnceListener");
+ if (once || prepend || !strcmp(method, "on") || !strcmp(method, "addListener")) {
+ ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined();
+ scr_dyn_check_listener(listener, "listener");
+ if (scr_exc_pending()) return NULL;
+ if (!argc || args[0]->kind != SCR_DYN_STR)
+ return scr_worker_refusal("Worker event names must be strings");
+ scr_emitter_release(scr_emitter_on_flex(view->events, args[0]->v.str, listener, once, prepend));
+ return scr_exc_pending() ? NULL : scr_dyn_retain(self);
+ }
+ if (!strcmp(method, "off") || !strcmp(method, "removeListener")) {
+ ScrDyn *listener = argc > 1 ? args[1] : scr_dyn_undefined();
+ scr_dyn_check_listener(listener, "listener");
+ if (scr_exc_pending()) return NULL;
+ if (!argc || args[0]->kind != SCR_DYN_STR)
+ return scr_worker_refusal("Worker event names must be strings");
+ scr_emitter_release(scr_emitter_off_dyn(view->events, args[0]->v.str, listener));
+ return scr_exc_pending() ? NULL : scr_dyn_retain(self);
+ }
+ if (!strcmp(method, "removeAllListeners")) {
+ bool all = !argc || args[0]->kind == SCR_DYN_UNDEF;
+ if (!all && args[0]->kind != SCR_DYN_STR)
+ return scr_worker_refusal("Worker event names must be strings");
+ scr_emitter_release(scr_emitter_remove_all(view->events, all ? NULL : args[0]->v.str, all));
+ return scr_exc_pending() ? NULL : scr_dyn_retain(self);
+ }
+ if (!strcmp(method, "listenerCount")) {
+ if (!argc || args[0]->kind != SCR_DYN_STR)
+ return scr_worker_refusal("Worker event names must be strings");
+ return scr_dyn_new_num(argc > 1 && args[1]->kind != SCR_DYN_UNDEF
+ ? scr_emitter_listener_count_dyn(view->events, args[0]->v.str, args[1])
+ : scr_emitter_listener_count(view->events, args[0]->v.str));
+ }
+ if (view->port && !strcmp(method, "close")) {
+ if (argc && args[0]->kind != SCR_DYN_UNDEF) {
+ scr_dyn_check_listener(args[0], "callback");
+ if (scr_exc_pending()) return NULL;
+ ScrStr *name = scr_str_new("close", 5);
+ scr_emitter_release(scr_emitter_on_flex(view->events, name, args[0], true, false));
+ scr_str_release(name);
+ }
+ if (!view->closed) view->closing = true;
+ view->closed = true;
+ scr_worker_port_close();
+ return scr_dyn_retain(scr_dyn_undefined());
+ }
+ if (view->port && !strcmp(method, "start")) {
+ view->started = true;
+ return scr_dyn_retain(scr_dyn_undefined());
+ }
+ return scr_worker_refusal("This Worker or MessagePort method is not supported yet");
+}
+
+static ScrDyn *scr_worker_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
+ ScrDyn *self = scr_dyn_this_get();
+ ScrDyn *result = NULL;
+ if (self->kind != SCR_DYN_HANDLE ||
+ (self->v.handle.tag != SCR_DYNH_WORKER && self->v.handle.tag != SCR_DYNH_MESSAGE_PORT)) {
+ static const char message[] = "Illegal invocation";
+ scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_INVALID_THIS");
+ } else {
+ size_t method = (size_t)scr_box_get_f64(closure->caps[0]);
+ result = scr_worker_view_invoke(self->v.handle.ptr, self, scr_worker_method_names[method], args, argc, "worker method");
+ }
+ scr_dyn_release(self);
+ return result;
+}
+
+static ScrDyn *scr_worker_view_get(void *ptr, const char *key, size_t length) {
+ ScrWorkerView *view = ptr;
+ if (!view->port && length == 8 && !memcmp(key, "threadId", 8))
+ return scr_dyn_new_num(view->closed ? -1 : (double)scr_worker_thread_id(view->thread));
+ for (size_t i = 0; i < sizeof scr_worker_methods / sizeof scr_worker_methods[0]; i++) {
+ const char *name = scr_worker_method_names[i];
+ if (strlen(name) != length || memcmp(name, key, length)) continue;
+ if ((view->port && !strcmp(name, "terminate")) ||
+ (!view->port && (!strcmp(name, "start") || !strcmp(name, "close") || !strcmp(name, "hasRef")))) return NULL;
+ if (!scr_worker_methods[i]) {
+ ScrClosure *closure = scr_closure_new(NULL, 1);
+ closure->caps[0] = scr_box_new(SCR_BOX_F64);
+ scr_box_set_f64(closure->caps[0], (double)i);
+ scr_worker_methods[i] = scr_dyn_new_func(closure, scr_worker_method_call, 0, "", name);
+ }
+ return scr_dyn_retain(scr_worker_methods[i]);
+ }
+ return NULL;
+}
+
+static bool scr_workers_pending(void) {
+ for (ScrWorkerView *view = scr_worker_views; view; view = view->next)
+ if (view->referenced) return true;
+ return scr_worker_port && (scr_worker_port->closing ||
+ (scr_worker_port->referenced && !scr_worker_port->closed));
+}
+
+static int scr_worker_events_pollfd(void) {
+ return scr_worker_events ? scr_mailbox_pollfd(scr_worker_events) : -1;
+}
+
+static bool scr_worker_events_ready(void) {
+ return (scr_worker_port && (scr_worker_port->closing ||
+ (scr_worker_port->started && scr_worker_port->queued_head))) ||
+ (scr_worker_events && scr_mailbox_pending(scr_worker_events));
+}
+
+static void scr_worker_events_wait(double milliseconds) {
+ if (!scr_worker_events_ready()) scr_mailbox_wait(scr_worker_events, milliseconds);
+}
+
+static void scr_worker_dispatch(void) {
+ ScrWorkerView *port = scr_worker_port;
+ if (port && port->closing) {
+ port->closing = false;
+ ScrStr *name = scr_str_new("close", 5);
+ scr_dyn_this_push(port, SCR_DYNH_MESSAGE_PORT);
+ scr_emitter_emit_flex(port->events, name, NULL, 0);
+ scr_dyn_this_pop();
+ scr_str_release(name);
+ if (scr_exc_pending()) return;
+ }
+ ScrMailEvent *event;
+ if (port && port->started && port->queued_head) {
+ event = port->queued_head;
+ port->queued_head = event->next;
+ if (!port->queued_head) port->queued_tail = NULL;
+ event->next = NULL;
+ } else event = scr_mailbox_take(scr_worker_events);
+ if (!event) return;
+ if (event->kind == SCR_WORKER_WAKE) { scr_mail_event_free(event); return; }
+ ScrWorkerView *view = scr_worker_port;
+ if (event->source) {
+ view = scr_worker_views;
+ while (view && scr_worker_thread_id(view->thread) != event->source) view = view->next;
+ }
+ if (!view) { scr_mail_event_free(event); return; }
+ if (view->port && event->kind == SCR_WORKER_MESSAGE) {
+ if (view->closed) { scr_mail_event_free(event); return; }
+ if (!view->started) {
+ if (view->queued_tail) view->queued_tail->next = event;
+ else view->queued_head = event;
+ view->queued_tail = event;
+ return;
+ }
+ }
+ scr_worker_view_retain(view);
+ const char *event_name = event->kind == SCR_WORKER_ONLINE ? "online" :
+ event->kind == SCR_WORKER_EXIT ? "exit" : event->kind == SCR_WORKER_ERROR ? "error" : "message";
+ ScrDyn *argument = event->message ? scr_message_decode(event->message) :
+ event->kind == SCR_WORKER_EXIT ? scr_dyn_new_num(event->code) : NULL;
+ if (event->kind == SCR_WORKER_EXIT) {
+ scr_worker_thread_join(view->thread);
+ view->closed = true;
+ if (view->termination) scr_promise_fulfill_ref(view->termination,
+ scr_dyn_new_num(event->code), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
+ ScrWorkerView **slot = &scr_worker_views;
+ while (*slot != view) slot = &(*slot)->next;
+ *slot = view->next;
+ view->next = NULL;
+ scr_worker_view_release(view); /* registry reference */
+ }
+ if (!scr_exc_pending()) {
+ ScrStr *name = scr_str_new(event_name, strlen(event_name));
+ scr_dyn_this_push(view, view->port ? SCR_DYNH_MESSAGE_PORT : SCR_DYNH_WORKER);
+ if (event->kind == SCR_WORKER_ERROR) scr_emitter_emit_error_flex(view->events, argument);
+ else scr_emitter_emit_flex(view->events, name, argument ? &argument : NULL, argument ? 1 : 0);
+ scr_dyn_this_pop();
+ scr_str_release(name);
+ }
+ scr_dyn_release(argument);
+ scr_worker_view_release(view);
+ scr_mail_event_free(event);
+}
+
+static void scr_worker_events_cleanup(void) {
+ scr_loop_set_workers(NULL, NULL, NULL, NULL, NULL);
+ scr_mailbox_close(scr_worker_events);
+ for (ScrWorkerView *view = scr_worker_views; view; view = view->next)
+ scr_worker_thread_terminate(view->thread);
+ while (scr_worker_views) {
+ ScrWorkerView *view = scr_worker_views;
+ scr_worker_views = view->next;
+ view->next = NULL;
+ scr_worker_thread_join(view->thread);
+ scr_worker_view_release(view);
+ }
+ scr_worker_view_release(scr_worker_port);
+ scr_worker_port = NULL;
+ for (size_t i = 0; i < sizeof scr_worker_methods / sizeof scr_worker_methods[0]; i++) {
+ scr_dyn_release(scr_worker_methods[i]);
+ scr_worker_methods[i] = NULL;
+ }
+ scr_mailbox_release(scr_worker_events);
+ scr_worker_events = NULL;
+}
+
+static bool scr_worker_events_install(void) {
+ if (scr_worker_events) return true;
+ ScrMailbox *inbox = scr_worker_inbox();
+ scr_worker_events = inbox ? scr_mailbox_retain(inbox) : scr_mailbox_new();
+ if (!scr_worker_events) return false;
+ if (inbox && !scr_worker_port) scr_worker_port = scr_worker_view_new(true);
+ static const ScrDynHandleOps worker_ops = {
+ .cls = "Worker", .retain = scr_worker_view_retain, .release = scr_worker_view_release,
+ .invoke = scr_worker_view_invoke, .get = scr_worker_view_get,
+ };
+ static const ScrDynHandleOps port_ops = {
+ .cls = "MessagePort", .retain = scr_worker_view_retain, .release = scr_worker_view_release,
+ .invoke = scr_worker_view_invoke, .get = scr_worker_view_get,
+ };
+ scr_dyn_handle_install(SCR_DYNH_WORKER, &worker_ops);
+ scr_dyn_handle_install(SCR_DYNH_MESSAGE_PORT, &port_ops);
+ if (scr_context_atexit(scr_worker_events_cleanup) != 0) scr_trap("scriptc: out of memory\n");
+ scr_loop_set_workers(scr_workers_pending, scr_worker_events_ready, scr_worker_dispatch,
+ scr_worker_events_pollfd, scr_worker_events_wait);
+ return true;
+}
+
+ScrDyn *scr_worker_parent_port(void) {
+ if (scr_context_is_main()) return scr_dyn_new_null();
+ if (!scr_worker_events_install()) return NULL;
+ if (!scr_worker_port) scr_worker_port = scr_worker_view_new(true);
+ return scr_dyn_new_handle(scr_worker_port, SCR_DYNH_MESSAGE_PORT);
+}
+
+static ScrArr *scr_worker_arguments(ScrStr *filename, ScrDyn *argv) {
+ if (argv->kind == SCR_DYN_TYPED_REF) {
+ ScrDyn *view = scr_dyn_typed_ref_materialize(argv);
+ ScrArr *result = !view || scr_exc_pending() ? NULL : scr_worker_arguments(filename, view);
+ scr_dyn_release(view);
+ return result;
+ }
+ if (argv->kind != SCR_DYN_UNDEF && argv->kind != SCR_DYN_ARR) {
+ scr_dyn_arg_type_fail("options.argv", "an instance of Array", argv);
+ return NULL;
+ }
+ ScrArr *arguments = scr_arr_new(SCR_ELEM_STR, 0);
+ scr_arr_push_ref(arguments, scr_str_retain(filename));
+ if (argv->kind == SCR_DYN_ARR) {
+ for (size_t i = 0; i < argv->v.arr.len; i++) {
+ ScrDyn *value = scr_dyn_arr_at(argv, (double)i);
+ ScrStr *text = scr_dyn_to_string(value, NULL);
+ scr_dyn_release(value);
+ if (!text || scr_exc_pending()) {
+ scr_str_release(text);
+ scr_arr_release(arguments);
+ return NULL;
+ }
+ if (memchr(text->data, 0, text->len)) {
+ scr_str_release(text);
+ scr_arr_release(arguments);
+ scr_worker_refusal("Worker argv strings containing null bytes are not supported yet");
+ return NULL;
+ }
+ scr_arr_push_ref(arguments, text);
+ }
+ }
+ return arguments;
+}
+
+static ScrDyn *scr_worker_new_options(double root, ScrStr *filename, ScrDyn *options) {
+ if (!scr_worker_events_install()) return NULL;
+ ScrDyn *data = scr_dyn_undefined();
+ ScrDyn *argv = scr_dyn_undefined();
+ if (options->kind != SCR_DYN_UNDEF) {
+ if (options->kind != SCR_DYN_OBJ) {
+ scr_dyn_arg_type_fail("options", "object", options);
+ return NULL;
+ }
+ for (size_t i = 0; i < options->v.obj.len; i++) {
+ const ScrDynEntry *entry = &options->v.obj.entries[i];
+ if (entry->accessor) return scr_worker_refusal("Worker option accessors are not supported yet");
+ if (entry->key_len == 10 && !memcmp(entry->key, "workerData", 10)) data = entry->value;
+ else if (entry->key_len == 4 && !memcmp(entry->key, "argv", 4)) argv = entry->value;
+ else return scr_worker_refusal("This Worker constructor option is not supported yet");
+ }
+ }
+ ScrArr *arguments = scr_worker_arguments(filename, argv);
+ if (!arguments) return NULL;
+ ScrMessage *wire = scr_message_encode(data);
+ if (!wire) { scr_arr_release(arguments); return NULL; }
+ ScrWorkerThread *thread = scr_worker_thread_start(sc_worker_entry, (uint32_t)root, wire, scr_worker_events, arguments);
+ scr_arr_release(arguments);
+ if (!thread) return NULL;
+ ScrWorkerView *view = scr_worker_view_new(false);
+ view->thread = thread;
+ view->next = scr_worker_views;
+ scr_worker_views = view;
+ return scr_dyn_new_handle(view, SCR_DYNH_WORKER);
+}
+
+ScrDyn *scr_worker_new(double root, ScrStr *filename, ScrDyn *options) {
+ ScrDyn *view = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : NULL;
+ ScrDyn *result = scr_exc_pending() ? NULL : scr_worker_new_options(root, filename, view ? view : options);
+ scr_dyn_release(view);
+ return result;
+}
+#endif
diff --git a/packages/runtime/src/scr_zlib.c b/packages/runtime/src/scr_zlib.c
index 7791d5341..8599d5fd9 100644
--- a/packages/runtime/src/scr_zlib.c
+++ b/packages/runtime/src/scr_zlib.c
@@ -23,6 +23,7 @@ static void scr_zlib_oom(void) {
}
ScrBytes *scr_zlib_deflate(const ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
uLong srcLen = (uLong)data->len;
uLong cap = compressBound(srcLen);
uint8_t *buf = malloc(cap ? cap : 1);
@@ -37,6 +38,7 @@ ScrBytes *scr_zlib_deflate(const ScrBytes *data) {
}
ScrBytes *scr_zlib_inflate(const ScrBytes *data) {
+ SCR_BYTES_SNAPSHOT(data);
z_stream zs;
memset(&zs, 0, sizeof zs);
if (inflateInit(&zs) != Z_OK) scr_zlib_oom();
@@ -100,6 +102,7 @@ static int scr_zlib_window_bits(int mode, bool inflating) {
}
ScrBytes *scr_zlib_deflate_mode(const ScrBytes *data, double mode, double level) {
+ SCR_BYTES_SNAPSHOT(data);
z_stream zs;
memset(&zs, 0, sizeof zs);
int lvl = (int)level;
@@ -126,6 +129,7 @@ ScrBytes *scr_zlib_deflate_mode(const ScrBytes *data, double mode, double level)
}
ScrBytes *scr_zlib_inflate_mode(const ScrBytes *data, double mode) {
+ SCR_BYTES_SNAPSHOT(data);
z_stream zs;
memset(&zs, 0, sizeof zs);
if (inflateInit2(&zs, scr_zlib_window_bits((int)mode, true)) != Z_OK) scr_zlib_oom();
@@ -177,6 +181,7 @@ ScrBytes *scr_zlib_inflate_mode(const ScrBytes *data, double mode) {
}
double scr_zlib_crc32(const ScrBytes *data, double value) {
+ SCR_BYTES_SNAPSHOT(data);
char recv[48], msg[160];
scr_num_received(value, recv);
if (!(isfinite(value) && trunc(value) == value)) {
@@ -321,6 +326,7 @@ static void scr_zlib_async_destroy(void *payload) {
void scr_zlib_codec_async(const ScrBytes *data, double mode, bool compressing,
ScrClosure *cb, ScrZlibBytesFn fn) {
+ SCR_BYTES_SNAPSHOT(data);
ScrZlibAsyncOp *op = calloc(1, sizeof *op);
if (!op) scr_zlib_oom();
op->data = malloc(data->len ? data->len : 1);
diff --git a/packages/runtime/test/array.test.ts b/packages/runtime/test/array.test.ts
index 97bed0a9b..af024ee3b 100644
--- a/packages/runtime/test/array.test.ts
+++ b/packages/runtime/test/array.test.ts
@@ -33,6 +33,12 @@ beforeAll(async () => {
join(testDir, "../src/scr_string.c"),
join(testDir, "../src/scr_number.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
+ join(testDir, "../src/scr_lib.c"),
+ join(testDir, "../src/scr_console.c"),
+ join(testDir, "../src/scr_path.c"),
+ join(testDir, "../src/scr_url.c"),
+ join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_object.c"),
diff --git a/packages/runtime/test/bytes.test.ts b/packages/runtime/test/bytes.test.ts
index 33dc5b3bb..42588f740 100644
--- a/packages/runtime/test/bytes.test.ts
+++ b/packages/runtime/test/bytes.test.ts
@@ -32,6 +32,7 @@ beforeAll(async () => {
join(testDir, "test_bytes.c"),
join(testDir, "../src/scr_bytes.test.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
join(testDir, "../src/scr_bytes_io.c"),
join(testDir, "../src/scr_zlib.c"),
join(testDir, "../src/scr_lib.c"),
diff --git a/packages/runtime/test/closure.test.ts b/packages/runtime/test/closure.test.ts
index e58f5e260..0f238ae3e 100644
--- a/packages/runtime/test/closure.test.ts
+++ b/packages/runtime/test/closure.test.ts
@@ -32,6 +32,12 @@ test("closure/box runtime: RC cascades clean under ASan + audit", async () => {
join(srcDir, "scr_number.c"),
join(srcDir, "scr_cycle.c"),
join(srcDir, "scr_bytes.c"),
+ join(srcDir, "scr_shared.c"),
+ join(srcDir, "scr_lib.c"),
+ join(srcDir, "scr_console.c"),
+ join(srcDir, "scr_path.c"),
+ join(srcDir, "scr_url.c"),
+ join(srcDir, "scr_url_params.c"),
join(srcDir, "scr_error.c"),
join(srcDir, "scr_exception.c"),
join(srcDir, "scr_object.c"),
diff --git a/packages/runtime/test/inspect.test.ts b/packages/runtime/test/inspect.test.ts
index 94403a5eb..658d3174c 100644
--- a/packages/runtime/test/inspect.test.ts
+++ b/packages/runtime/test/inspect.test.ts
@@ -36,6 +36,12 @@ beforeAll(async () => {
join(testDir, "../src/scr_number.c"),
join(testDir, "../src/scr_array.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
+ join(testDir, "../src/scr_lib.c"),
+ join(testDir, "../src/scr_console.c"),
+ join(testDir, "../src/scr_path.c"),
+ join(testDir, "../src/scr_url.c"),
+ join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_object.c"),
diff --git a/packages/runtime/test/json.test.ts b/packages/runtime/test/json.test.ts
index 60c021845..ba9ffe20f 100644
--- a/packages/runtime/test/json.test.ts
+++ b/packages/runtime/test/json.test.ts
@@ -43,6 +43,7 @@ beforeAll(async () => {
join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_path.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []),
]);
});
diff --git a/packages/runtime/test/lib.test.ts b/packages/runtime/test/lib.test.ts
index 88c9c86ec..729ea3605 100644
--- a/packages/runtime/test/lib.test.ts
+++ b/packages/runtime/test/lib.test.ts
@@ -46,6 +46,7 @@ beforeAll(async () => {
join(testDir, "../src/scr_json.c"),
join(testDir, "../src/scr_bigint.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
...(process.platform === "linux" ? ["-lm"] : []),
]);
scratch = await mkdtemp(join(tmpdir(), "scriptc-lib-"));
diff --git a/packages/runtime/test/map.test.ts b/packages/runtime/test/map.test.ts
index 72d0c84ac..cc8830c15 100644
--- a/packages/runtime/test/map.test.ts
+++ b/packages/runtime/test/map.test.ts
@@ -38,6 +38,12 @@ beforeAll(async () => {
join(testDir, "../src/scr_closure.c"),
join(testDir, "../src/scr_object.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
+ join(testDir, "../src/scr_lib.c"),
+ join(testDir, "../src/scr_console.c"),
+ join(testDir, "../src/scr_path.c"),
+ join(testDir, "../src/scr_url.c"),
+ join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_exception.c"),
...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []),
diff --git a/packages/runtime/test/path.test.ts b/packages/runtime/test/path.test.ts
index 51569fc46..ada14c451 100644
--- a/packages/runtime/test/path.test.ts
+++ b/packages/runtime/test/path.test.ts
@@ -36,6 +36,11 @@ beforeAll(async () => {
join(testDir, "../src/scr_bigint.c"),
join(testDir, "../src/scr_closure.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
+ join(testDir, "../src/scr_lib.c"),
+ join(testDir, "../src/scr_console.c"),
+ join(testDir, "../src/scr_url.c"),
+ join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_object.c"),
diff --git a/packages/runtime/test/regex.test.ts b/packages/runtime/test/regex.test.ts
index 6f3a5f328..1bd037180 100644
--- a/packages/runtime/test/regex.test.ts
+++ b/packages/runtime/test/regex.test.ts
@@ -51,6 +51,7 @@ beforeAll(async () => {
join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_path.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []),
]);
}, 60_000);
diff --git a/packages/runtime/test/runtime.test.ts b/packages/runtime/test/runtime.test.ts
index 4a3f3e5cb..b7ec196be 100644
--- a/packages/runtime/test/runtime.test.ts
+++ b/packages/runtime/test/runtime.test.ts
@@ -14,6 +14,7 @@ const RUNTIME_SOURCES = [
"scr_string.c",
"scr_array.c",
"scr_bytes.c",
+ "scr_shared.c",
"scr_bytes_io.c",
"scr_map.c",
"scr_closure.c",
diff --git a/packages/runtime/test/string.test.ts b/packages/runtime/test/string.test.ts
index 30ddf80c8..28f74699f 100644
--- a/packages/runtime/test/string.test.ts
+++ b/packages/runtime/test/string.test.ts
@@ -37,6 +37,12 @@ beforeAll(async () => {
join(testDir, "../src/scr_bigint.c"),
join(testDir, "../src/scr_closure.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
+ join(testDir, "../src/scr_lib.c"),
+ join(testDir, "../src/scr_console.c"),
+ join(testDir, "../src/scr_path.c"),
+ join(testDir, "../src/scr_url.c"),
+ join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_object.c"),
diff --git a/packages/runtime/test/tonumber.test.ts b/packages/runtime/test/tonumber.test.ts
index db3664ad9..6278dbec0 100644
--- a/packages/runtime/test/tonumber.test.ts
+++ b/packages/runtime/test/tonumber.test.ts
@@ -44,6 +44,12 @@ test("scr_string_to_number matches Node Number(s) on committed oracle cases", as
join(testDir, "../src/scr_bigint.c"),
join(testDir, "../src/scr_closure.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
+ join(testDir, "../src/scr_lib.c"),
+ join(testDir, "../src/scr_console.c"),
+ join(testDir, "../src/scr_path.c"),
+ join(testDir, "../src/scr_url.c"),
+ join(testDir, "../src/scr_url_params.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_object.c"),
diff --git a/packages/runtime/test/url.test.ts b/packages/runtime/test/url.test.ts
index 09c2f60f9..672d69e25 100644
--- a/packages/runtime/test/url.test.ts
+++ b/packages/runtime/test/url.test.ts
@@ -36,6 +36,9 @@ beforeAll(async () => {
join(testDir, "../src/scr_bigint.c"),
join(testDir, "../src/scr_closure.c"),
join(testDir, "../src/scr_bytes.c"),
+ join(testDir, "../src/scr_shared.c"),
+ join(testDir, "../src/scr_lib.c"),
+ join(testDir, "../src/scr_console.c"),
join(testDir, "../src/scr_error.c"),
join(testDir, "../src/scr_exception.c"),
join(testDir, "../src/scr_object.c"),
diff --git a/scripts/test-ts7.mjs b/scripts/test-ts7.mjs
index 599fa7420..bad8e557b 100644
--- a/scripts/test-ts7.mjs
+++ b/scripts/test-ts7.mjs
@@ -4,9 +4,28 @@ import { readdirSync, readFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
+import { allOrderEntries } from "./ts7-order-fixtures.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const baseline = JSON.parse(
+ readFileSync(resolve(repoRoot, "packages/compiler/test/ts7/baselines/order-parity.json"), "utf8"),
+);
+const missingBaselines = allOrderEntries(repoRoot)
+ .map((path) => relative(repoRoot, path).replaceAll("\\", "/"))
+ .filter((path) => !Object.hasOwn(baseline.entries, `/${path}`));
+if (missingBaselines.length > 0) {
+ console.error(`Missing TypeScript preflight/order baselines:\n${missingBaselines.join("\n")}`);
+ console.error(
+ "Record the new fixtures with SCRIPTC_UPDATE_BASELINES=1 pnpm exec vitest run packages/compiler/test/ts7/order-parity.test.ts, review the diff, then run the affected parity batches before opening a PR.",
+ );
+ process.exit(1);
+}
+if (process.argv.includes("--baselines-only")) {
+ console.log("Every TypeScript preflight/order fixture has a recorded baseline.");
+ process.exit(0);
+}
+
const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([
"packages/compiler/src/frontend/comptime-node.ts",
"packages/compiler/src/frontend/ts7/world-check.ts",
diff --git a/scripts/ts7-order-fixtures.mjs b/scripts/ts7-order-fixtures.mjs
new file mode 100644
index 000000000..a22a80692
--- /dev/null
+++ b/scripts/ts7-order-fixtures.mjs
@@ -0,0 +1,21 @@
+import { globSync } from "node:fs";
+import { join } from "node:path";
+
+export function orderEntriesUnder(repoRoot, dir) {
+ return ["ts", "js", "mjs", "cjs"]
+ .flatMap((ext) => [
+ ...globSync(join(repoRoot, dir, `*.${ext}`)),
+ ...globSync(join(repoRoot, dir, `*/main.${ext}`)),
+ ])
+ .sort();
+}
+
+export function allOrderEntries(repoRoot) {
+ return [
+ ...orderEntriesUnder(repoRoot, "tests/corpus"),
+ ...orderEntriesUnder(repoRoot, "tests/diagnostics"),
+ ...orderEntriesUnder(repoRoot, "tests/fixtures/npm/cases"),
+ ...globSync(join(repoRoot, "tests/fixtures/strictness/*/main.ts")).sort(),
+ ...globSync(join(repoRoot, "tests/fixtures/node-types/*.ts")).sort(),
+ ];
+}
diff --git a/tests/corpus/1524-atomics-sleep-static.ts b/tests/corpus/1524-atomics-sleep-static.ts
index c218424f6..8068fb4d5 100644
--- a/tests/corpus/1524-atomics-sleep-static.ts
+++ b/tests/corpus/1524-atomics-sleep-static.ts
@@ -1,6 +1,6 @@
// The RouteStore synchronous-sleep idiom, whole: a STATIC readonly class
// field holding `new Int32Array(new SharedArrayBuffer(4))`, slept on with
-// Atomics.wait(buf, 0, 0, ms). No threads exist, so the wait always times
+// Atomics.wait(buf, 0, 0, ms). This program has no notifying worker, so it times
// out — a true synchronous sleep. Timing facts are BOUNDED (slept at
// least ~ms, well under a generous ceiling), never exact tick counts.
// Plus the i32 element semantics (sign, ToInt32 wrap) and the static
diff --git a/tests/corpus/atomics-integer-operations.ts b/tests/corpus/atomics-integer-operations.ts
new file mode 100644
index 000000000..52f3a4350
--- /dev/null
+++ b/tests/corpus/atomics-integer-operations.ts
@@ -0,0 +1,28 @@
+function exercise(values: T): void {
+ console.log(Atomics.store(values, 0, 257.9), Atomics.load(values, 0));
+ console.log(Atomics.exchange(values, 0, -3), Atomics.load(values, 0));
+ console.log(Atomics.compareExchange(values, 0, -3, 65535), Atomics.load(values, 0));
+ console.log(Atomics.compareExchange(values, 0, 7, 99), Atomics.load(values, 0));
+ console.log(Atomics.add(values, 0, 2), Atomics.sub(values, 0, 3), Atomics.load(values, 0));
+ console.log(Atomics.and(values, 0, 15), Atomics.or(values, 0, 16), Atomics.xor(values, 0, 3), Atomics.load(values, 0));
+ console.log(Atomics.store(values, NaN, NaN), Atomics.store(values, -0.5, Infinity), Atomics.load(values, 0));
+ console.log(Atomics.store(values, 0.9, -0), Object.is(Atomics.load(values, 0), -0));
+ for (const index of [-1, 1, Infinity]) {
+ try { Atomics.load(values, index); }
+ catch (error) { console.log(error instanceof RangeError); }
+ }
+}
+
+exercise(new Int8Array(new SharedArrayBuffer(1)));
+exercise(new Uint8Array(new SharedArrayBuffer(1)));
+exercise(new Int16Array(new SharedArrayBuffer(2)));
+exercise(new Uint16Array(new SharedArrayBuffer(2)));
+exercise(new Int32Array(new SharedArrayBuffer(4)));
+exercise(new Uint32Array(new SharedArrayBuffer(4)));
+const local = new Int32Array(1);
+console.log("local", Atomics.add(local, 0, 2), local[0], Atomics.notify(local, 0));
+try { Atomics.wait(local, 0, 2, 0); }
+catch (error) { console.log("local wait", error instanceof TypeError); }
+const shared = new Int32Array(new SharedArrayBuffer(4));
+console.log("wait", Atomics.wait(shared, 0, 1, undefined), Atomics.wait(shared, 0, 0, -1));
+console.log("notify", Atomics.notify(shared, 0, undefined), Atomics.notify(shared, 0, NaN));
diff --git a/tests/corpus/native-worker-buffer-consumers.ts b/tests/corpus/native-worker-buffer-consumers.ts
new file mode 100644
index 000000000..37644bc95
--- /dev/null
+++ b/tests/corpus/native-worker-buffer-consumers.ts
@@ -0,0 +1,37 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+import { createHash } from "node:crypto";
+import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+if (isMainThread) {
+ const storage = new SharedArrayBuffer(64);
+ const bytes = Buffer.from(storage);
+ const values = new Uint8Array(storage);
+ bytes.fill(65);
+ const directory = mkdtempSync(join(tmpdir(), "worker-bytes-"));
+ const file = join(directory, "bytes");
+ const worker = new Worker(new URL(import.meta.url), {workerData: storage});
+ worker.once("message", () => {
+ let valid = true;
+ for (let i = 0; i < 100; i++) {
+ const text = bytes.toString("ascii");
+ valid = valid && text.length === 64 && /^[AB]+$/.test(text);
+ valid = valid && createHash("sha256").update(bytes).digest("hex").length === 64;
+ writeFileSync(file, bytes);
+ valid = valid && readFileSync(file).length === 64;
+ const sorted = values.toSorted((a: number, b: number) => {
+ Atomics.load(values, 0);
+ return a - b;
+ });
+ valid = valid && sorted.length === 64;
+ }
+ console.log("shared consumers", valid, bytes.buffer === storage);
+ rmSync(directory, {recursive: true});
+ });
+ worker.once("exit", (code: number) => console.log("exit", code));
+} else {
+ const values = new Uint8Array(workerData as SharedArrayBuffer);
+ parentPort!.postMessage("ready");
+ for (let i = 0; i < 200000; i++) Atomics.store(values, i % 64, 65 + (i % 2));
+}
diff --git a/tests/corpus/native-worker-clone-errors.ts b/tests/corpus/native-worker-clone-errors.ts
new file mode 100644
index 000000000..1d66387b8
--- /dev/null
+++ b/tests/corpus/native-worker-clone-errors.ts
@@ -0,0 +1,17 @@
+import { Worker, isMainThread, parentPort } from "node:worker_threads";
+
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url));
+ try {
+ worker.postMessage(() => {});
+ } catch (error) {
+ if (error instanceof Error) console.log("clone", error.name);
+ else throw error;
+ }
+ const transfer: unknown[] = [];
+ worker.postMessage(42, transfer);
+ worker.on("message", (value: unknown) => console.log("reply", value));
+ worker.on("exit", (code: number) => console.log("exit", code));
+} else {
+ parentPort!.once("message", (value: unknown) => parentPort!.postMessage(value));
+}
diff --git a/tests/corpus/native-worker-commonjs.cjs b/tests/corpus/native-worker-commonjs.cjs
new file mode 100644
index 000000000..81bbef4b5
--- /dev/null
+++ b/tests/corpus/native-worker-commonjs.cjs
@@ -0,0 +1,13 @@
+const { Worker, isMainThread, parentPort, workerData } = require("node:worker_threads");
+
+if (isMainThread) {
+ const worker = new Worker(__filename, { workerData: 41 });
+ worker.on("message", (value) => console.log("result", value));
+ worker.on("exit", async (code) => {
+ // A completed Worker discards even uncloneable payloads.
+ worker.postMessage(() => {});
+ console.log("exit", code, worker.threadId, await worker.terminate());
+ });
+} else {
+ parentPort.postMessage(workerData + 1);
+}
diff --git a/tests/corpus/native-worker-delayed-messages.ts b/tests/corpus/native-worker-delayed-messages.ts
new file mode 100644
index 000000000..e27b47b72
--- /dev/null
+++ b/tests/corpus/native-worker-delayed-messages.ts
@@ -0,0 +1,20 @@
+import { Worker, isMainThread, parentPort } from "node:worker_threads";
+
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url));
+ worker.on("message", (value: unknown) => console.log(JSON.stringify(value)));
+ worker.on("exit", (code: number) => console.log("exit", code));
+ worker.postMessage(20);
+ worker.postMessage(22);
+} else {
+ const values: number[] = [];
+ setTimeout(() => {
+ parentPort!.on("message", (value: unknown) => {
+ values.push(value as number);
+ if (values.length === 2) {
+ parentPort!.postMessage(values);
+ parentPort!.close();
+ }
+ });
+ }, 20);
+}
diff --git a/tests/corpus/native-worker-environment.ts b/tests/corpus/native-worker-environment.ts
new file mode 100644
index 000000000..529a9d3c2
--- /dev/null
+++ b/tests/corpus/native-worker-environment.ts
@@ -0,0 +1,28 @@
+import { Worker, isMainThread, parentPort, workerData, threadId } from "node:worker_threads";
+
+if (isMainThread) {
+ process.env.SCRIPTC_WORKER_ENV_TEST = "parent";
+ const worker = new Worker(new URL(import.meta.url), { workerData: 1, argv: ["one", 2, true, "café"] });
+ worker.on("message", (value: unknown) => console.log(JSON.stringify(value)));
+ worker.on("exit", (code: number) => console.log("exit", code, process.env.SCRIPTC_WORKER_ENV_TEST));
+} else if ((workerData as number) === 1) {
+ const before = process.env.SCRIPTC_WORKER_ENV_TEST;
+ const args = process.argv.slice(2);
+ process.env.SCRIPTC_WORKER_ENV_TEST = "child";
+ const worker = new Worker(new URL(import.meta.url), { workerData: 2 });
+ worker.on("message", (value: unknown) => {
+ parentPort!.postMessage({ args, before, after: process.env.SCRIPTC_WORKER_ENV_TEST, nested: value });
+ });
+} else {
+ const failures: string[] = [];
+ try { process.chdir("."); } catch (error) {
+ if (error instanceof Error) failures.push(error.message);
+ else throw error;
+ }
+ const mask = process.umask();
+ try { process.umask(mask); } catch (error) {
+ if (error instanceof Error) failures.push(error.message);
+ else throw error;
+ }
+ parentPort!.postMessage({ environment: process.env.SCRIPTC_WORKER_ENV_TEST, worker: threadId > 0, failures });
+}
diff --git a/tests/corpus/native-worker-error.ts b/tests/corpus/native-worker-error.ts
new file mode 100644
index 000000000..16648ab80
--- /dev/null
+++ b/tests/corpus/native-worker-error.ts
@@ -0,0 +1,6 @@
+import { Worker, isMainThread } from "node:worker_threads";
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url));
+ worker.on("error", (error: Error) => console.log(error.name, error.message, error instanceof TypeError));
+ worker.on("exit", (code: number) => console.log("exit", code));
+} else { throw new TypeError("worker failed"); }
diff --git a/tests/corpus/native-worker-exit-listeners.ts b/tests/corpus/native-worker-exit-listeners.ts
new file mode 100644
index 000000000..c100d432f
--- /dev/null
+++ b/tests/corpus/native-worker-exit-listeners.ts
@@ -0,0 +1,26 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+
+if (isMainThread) {
+ for (const mode of ["nested", "throw", "natural"]) {
+ await new Promise((resolve) => {
+ const worker = new Worker(new URL(import.meta.url), {workerData: mode});
+ let message = -1;
+ let messages = 0;
+ let failure = "none";
+ // Message and error use independent channels; both must arrive before exit.
+ worker.on("message", (value: number) => { message = value; messages++; });
+ worker.on("error", (error: Error) => { failure = error.message; });
+ worker.on("exit", (code: number) => {
+ console.log(mode, "exit", code, "messages", messages, message, "error", failure);
+ resolve();
+ });
+ });
+ }
+} else {
+ process.on("exit", (code: number) => {
+ parentPort!.postMessage(code);
+ if (workerData === "nested") process.exit(3);
+ if (workerData === "throw") throw new Error("exit listener failed");
+ });
+ if (workerData !== "natural") process.exit(7);
+}
diff --git a/tests/corpus/native-worker-io.ts b/tests/corpus/native-worker-io.ts
new file mode 100644
index 000000000..16c4486ba
--- /dev/null
+++ b/tests/corpus/native-worker-io.ts
@@ -0,0 +1,48 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+import { execFile } from "node:child_process";
+import { mkdtempSync, writeFileSync, readFileSync, rename, rmSync, watch } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+
+if (isMainThread) {
+ const tasks: Promise[] = [];
+ for (let id = 0; id < 4; id++) {
+ tasks.push(new Promise((resolve, reject) => {
+ const worker = new Worker(new URL(import.meta.url), {workerData: id});
+ let message = "";
+ worker.once("message", (value: string) => { message = value; });
+ worker.once("error", reject);
+ worker.once("exit", (code: number) => {
+ if (code !== 0) reject(new Error("worker failed"));
+ else resolve(message);
+ });
+ }));
+ }
+ console.log(JSON.stringify(await Promise.all(tasks)));
+} else {
+ const id = workerData as number;
+ const directory = mkdtempSync(join(tmpdir(), "worker-io-"));
+ const before = join(directory, "before");
+ const after = join(directory, "after");
+ const watcher = watch(directory, () => {});
+ watcher.close();
+ writeFileSync(before, "file-" + id);
+ await new Promise((resolve, reject) => {
+ rename(before, after, (error) => {
+ if (error) reject(error);
+ else resolve();
+ });
+ });
+ const output = await new Promise((resolve, reject) => {
+ execFile("/bin/sh", ["-c", "printf child"], (error, stdout) => {
+ if (error) reject(error);
+ else resolve(stdout);
+ });
+ });
+ const missing = await new Promise((resolve) => {
+ execFile("definitely-not-a-worker-command", (error) => resolve(error !== null));
+ });
+ const text = readFileSync(after, "utf8");
+ rmSync(directory, {recursive: true});
+ parentPort!.postMessage(text + ":" + output + ":" + missing);
+}
diff --git a/tests/corpus/native-worker-loop-cancellation.ts b/tests/corpus/native-worker-loop-cancellation.ts
new file mode 100644
index 000000000..1fe4335c6
--- /dev/null
+++ b/tests/corpus/native-worker-loop-cancellation.ts
@@ -0,0 +1,22 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+
+if (isMainThread) {
+ for (const kind of ["for", "do", "for-of"]) {
+ await new Promise((resolve) => {
+ const worker = new Worker(new URL(import.meta.url), {workerData: kind});
+ worker.once("message", () => { worker.terminate(); });
+ worker.once("exit", (code: number) => { console.log(kind, code); resolve(); });
+ });
+ }
+} else {
+ parentPort!.postMessage("ready");
+ try {
+ if (workerData === "for") { for (;;) { continue; } }
+ else if (workerData === "do") { do { continue; } while (true); }
+ else {
+ const growing = [1];
+ for (const value of growing) growing.push(value);
+ }
+ } catch { console.log("unexpected catch"); }
+ finally { console.log("unexpected finally"); }
+}
diff --git a/tests/corpus/native-worker-message-graphs.ts b/tests/corpus/native-worker-message-graphs.ts
new file mode 100644
index 000000000..d5f8da117
--- /dev/null
+++ b/tests/corpus/native-worker-message-graphs.ts
@@ -0,0 +1,39 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+
+if (isMainThread) {
+ const shared = { value: 17 };
+ const buffer = new ArrayBuffer(16);
+ const bytes = new Uint16Array(buffer, 4, 3);
+ bytes[0] = 91;
+ const root: Record = {
+ first: shared,
+ second: shared,
+ sparse: [, undefined, , shared],
+ negativeZero: -0,
+ nan: NaN,
+ infinity: Infinity,
+ text: "a\0😀",
+ integer: 123456789012345678901234567890n,
+ buffer,
+ bytes,
+ again: bytes,
+ error: new TypeError("problem", { cause: shared }),
+ };
+ root.self = root;
+ const worker = new Worker(new URL(import.meta.url), { workerData: root });
+ worker.on("message", (value: unknown) => console.log(JSON.stringify(value)));
+ worker.on("exit", (code: number) => console.log("exit", code, "original", bytes[0]));
+} else {
+ const root = workerData;
+ root.bytes[0] = 42;
+ parentPort!.postMessage({
+ self: root.self === root,
+ alias: root.first === root.second && root.sparse[3] === root.first,
+ holes: [0 in root.sparse, 1 in root.sparse, 2 in root.sparse],
+ numbers: [Object.is(root.negativeZero, -0), Number.isNaN(root.nan), root.infinity === Infinity],
+ text: root.text,
+ integer: String(root.integer),
+ views: [root.bytes === root.again, root.bytes.buffer === root.buffer, root.bytes.byteOffset],
+ error: [root.error instanceof TypeError, root.error.message, root.error.cause === root.first],
+ });
+}
diff --git a/tests/corpus/native-worker-messages.ts b/tests/corpus/native-worker-messages.ts
new file mode 100644
index 000000000..bef275bbe
--- /dev/null
+++ b/tests/corpus/native-worker-messages.ts
@@ -0,0 +1,15 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+let moduleCounter = 0;
+function bump(): number { return ++moduleCounter; }
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url), { workerData: { seed: 41 } });
+ worker.on("message", (value: unknown) => console.log(JSON.stringify(value)));
+ worker.on("exit", (code: number) => console.log("exit", code, "main", bump()));
+ worker.postMessage(1);
+} else {
+ const port = parentPort!;
+ const data = workerData as { seed: number };
+ port.once("message", (value: unknown) => {
+ port.postMessage({ result: data.seed + (value as number), counter: bump() });
+ });
+}
diff --git a/tests/corpus/native-worker-port-lifetime.ts b/tests/corpus/native-worker-port-lifetime.ts
new file mode 100644
index 000000000..be7d283d8
--- /dev/null
+++ b/tests/corpus/native-worker-port-lifetime.ts
@@ -0,0 +1,39 @@
+import { Worker, isMainThread, parentPort } from "node:worker_threads";
+
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url));
+ let reply = "";
+ worker.on("message", (value: unknown) => { reply = JSON.stringify(value); });
+ worker.on("exit", (code: number) => console.log(reply, "exit", code));
+ // Delivery must survive posting before the worker has initialized.
+ worker.postMessage(42);
+} else {
+ const port = parentPort!;
+ port.start();
+ const states: boolean[] = [port.hasRef()];
+ const noop = () => {};
+ port.ref();
+ port.on("close", noop);
+ port.off("close", noop);
+ states.push(port.hasRef());
+ port.on("message", noop);
+ port.unref();
+ port.on("message", noop);
+ states.push(port.hasRef());
+ port.removeAllListeners("message");
+ port.ref();
+ port.removeAllListeners("message");
+ states.push(port.hasRef());
+ port.unref();
+ port.once("message", (value: unknown) => {
+ states.push(port.hasRef());
+ port.ref();
+ states.push(port.hasRef());
+ const post = port.postMessage;
+ post.call(port, { states, value });
+ port.close(() => {
+ if (port.hasRef()) throw new Error("closed port retained its reference");
+ });
+ });
+ states.push(port.hasRef());
+}
diff --git a/tests/corpus/native-worker-process-exit.ts b/tests/corpus/native-worker-process-exit.ts
new file mode 100644
index 000000000..26c547461
--- /dev/null
+++ b/tests/corpus/native-worker-process-exit.ts
@@ -0,0 +1,11 @@
+import { Worker, isMainThread, parentPort } from "node:worker_threads";
+
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url));
+ worker.on("message", (value: unknown) => console.log("message", value));
+ worker.on("exit", (code: number) => console.log("exit", code, "parent alive"));
+} else {
+ process.on("exit", (code: number) => parentPort!.postMessage(code));
+ process.exit(7);
+ console.log("unreachable");
+}
diff --git a/tests/corpus/native-worker-references.ts b/tests/corpus/native-worker-references.ts
new file mode 100644
index 000000000..03a072c59
--- /dev/null
+++ b/tests/corpus/native-worker-references.ts
@@ -0,0 +1,17 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+
+if (isMainThread) {
+ await new Promise((resolve) => {
+ const worker = new Worker(new URL(import.meta.url));
+ console.log("handles", typeof worker.unref(), typeof worker.ref(), worker.threadId > 0);
+ worker.once("online", () => console.log("online"));
+ worker.once("message", (value: unknown) => console.log("message", value));
+ worker.once("exit", (code: number) => { console.log("exit", code, worker.threadId); resolve(); });
+ });
+ new Worker(new URL(import.meta.url), {workerData: "idle"}).unref();
+ console.log("unreferenced worker does not keep parent alive");
+} else if (workerData === "idle") {
+ setInterval(() => {}, 1000);
+} else {
+ parentPort!.postMessage(42);
+}
diff --git a/tests/corpus/native-worker-roots/main.ts b/tests/corpus/native-worker-roots/main.ts
new file mode 100644
index 000000000..f60c343b7
--- /dev/null
+++ b/tests/corpus/native-worker-roots/main.ts
@@ -0,0 +1,16 @@
+import { Worker } from "node:worker_threads";
+
+for (const round of [1, 2]) {
+ const jobs: Promise[] = [
+ new Promise((resolve) => {
+ const worker = new Worker(new URL("./sum.ts", import.meta.url), {workerData: round});
+ worker.once("message", (value: number) => resolve(value));
+ }),
+ new Promise((resolve) => {
+ const worker = new Worker(new URL("./product.ts", import.meta.url), {workerData: round});
+ worker.once("message", (value: number) => resolve(value));
+ }),
+ ];
+ const result = await Promise.all(jobs);
+ console.log(JSON.stringify(result));
+}
diff --git a/tests/corpus/native-worker-roots/product.ts b/tests/corpus/native-worker-roots/product.ts
new file mode 100644
index 000000000..38d7f914a
--- /dev/null
+++ b/tests/corpus/native-worker-roots/product.ts
@@ -0,0 +1,4 @@
+import * as threads from "node:worker_threads";
+let local = 10;
+local *= threads.workerData as number;
+threads.parentPort!.postMessage(threads.isMainThread ? -1 : local);
diff --git a/tests/corpus/native-worker-roots/sum.ts b/tests/corpus/native-worker-roots/sum.ts
new file mode 100644
index 000000000..3745fb638
--- /dev/null
+++ b/tests/corpus/native-worker-roots/sum.ts
@@ -0,0 +1,4 @@
+import { parentPort, workerData, isMainThread } from "node:worker_threads";
+let local = 10;
+local += workerData as number;
+parentPort!.postMessage(isMainThread ? -1 : local);
diff --git a/tests/corpus/native-worker-shared-views.ts b/tests/corpus/native-worker-shared-views.ts
new file mode 100644
index 000000000..9cd45348f
--- /dev/null
+++ b/tests/corpus/native-worker-shared-views.ts
@@ -0,0 +1,25 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+if (isMainThread) {
+ const buffer = new SharedArrayBuffer(32);
+ const values = new Int32Array(buffer);
+ const view = new DataView(buffer, 16, 8);
+ view.setInt32(0, 123, true);
+ const worker = new Worker(new URL(import.meta.url), {workerData: {buffer, values, view}});
+ worker.on("message", (value: unknown) => {
+ if (value === "ready") {
+ for (let i = 0; i < 10000; i++) Atomics.add(values, 0, 1);
+ Atomics.store(values, 1, 1);
+ Atomics.notify(values, 1);
+ } else console.log(JSON.stringify(value));
+ });
+ worker.on("exit", (code: number) => console.log("exit", code, values[0], view.getInt32(0, true), buffer.byteLength));
+} else {
+ const data = workerData as {buffer: SharedArrayBuffer, values: Int32Array, view: DataView};
+ const values = data.values;
+ parentPort!.postMessage("ready");
+ for (let i = 0; i < 10000; i++) Atomics.add(values, 0, 1);
+ while (Atomics.load(values, 1) === 0) Atomics.wait(values, 1, 0);
+ data.view.setInt32(0, data.view.getInt32(0, true) + 1, true);
+ const alias = new Int32Array(data.buffer, 16, 1);
+ parentPort!.postMessage({shared: data.buffer instanceof SharedArrayBuffer, alias: alias[0], same: data.values.buffer === data.buffer, offset: data.view.byteOffset});
+}
diff --git a/tests/corpus/native-worker-terminate-loop.ts b/tests/corpus/native-worker-terminate-loop.ts
new file mode 100644
index 000000000..408eb5964
--- /dev/null
+++ b/tests/corpus/native-worker-terminate-loop.ts
@@ -0,0 +1,12 @@
+import { Worker, isMainThread, parentPort } from "node:worker_threads";
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url));
+ worker.on("message", async () => {
+ console.log("ready");
+ console.log("terminated", await worker.terminate());
+ });
+ worker.on("exit", (code: number) => console.log("exit", code));
+} else {
+ parentPort!.postMessage("ready");
+ try { while (true) {} } catch { console.log("caught"); } finally { console.log("finally"); }
+}
diff --git a/tests/corpus/native-worker-terminate-suspended.ts b/tests/corpus/native-worker-terminate-suspended.ts
new file mode 100644
index 000000000..5cc34d4aa
--- /dev/null
+++ b/tests/corpus/native-worker-terminate-suspended.ts
@@ -0,0 +1,21 @@
+import { Worker, isMainThread, parentPort } from "node:worker_threads";
+async function wait(): Promise {
+ const value = ["owned", "stack"];
+ try { await new Promise(() => {}); console.log(value.join(" ")); }
+ finally { console.log("finally"); }
+}
+function* values(): Generator {
+ const value = ["owned", "generator"];
+ try { yield value.join(" "); }
+ finally { console.log("generator finally"); }
+}
+if (isMainThread) {
+ const worker = new Worker(new URL(import.meta.url));
+ worker.on("message", async () => { console.log("ready"); console.log("terminated", await worker.terminate()); });
+ worker.on("exit", (code: number) => console.log("exit", code));
+} else {
+ wait();
+ const generator = values(); generator.next();
+ parentPort!.on("message", () => {});
+ parentPort!.postMessage("ready");
+}
diff --git a/tests/corpus/native-worker-terminate-wait.ts b/tests/corpus/native-worker-terminate-wait.ts
new file mode 100644
index 000000000..e2892ccde
--- /dev/null
+++ b/tests/corpus/native-worker-terminate-wait.ts
@@ -0,0 +1,22 @@
+import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
+
+if (isMainThread) {
+ const shared = new SharedArrayBuffer(4);
+ const worker = new Worker(new URL(import.meta.url), { workerData: shared });
+ worker.on("message", () => {
+ // The worker may already be parked, or may reach wait after cancellation.
+ worker.terminate().then((code: number) => console.log("terminated", code));
+ });
+ worker.on("exit", (code: number) => console.log("exit", code));
+} else {
+ const values = new Int32Array(workerData as SharedArrayBuffer);
+ parentPort!.postMessage("ready");
+ try {
+ Atomics.wait(values, 0, 0, undefined);
+ console.log("unexpected wake");
+ } catch {
+ console.log("unexpected catch");
+ } finally {
+ console.log("unexpected finally");
+ }
+}
diff --git a/tests/fixtures/self-hosting/class-types.ts b/tests/fixtures/self-hosting/class-types.ts
index 6b652a400..0d3528eb9 100644
--- a/tests/fixtures/self-hosting/class-types.ts
+++ b/tests/fixtures/self-hosting/class-types.ts
@@ -8,7 +8,7 @@ const kept: IrType = { kind: "object", className: "Registered" };
const expr: IrExpr = { kind: "varRef", localId: "x", type: missing, loc };
const nested: IrType = { kind: "func", params: [{ kind: "array", elem: missing }],
ret: { kind: "generator", yieldT: missing, retT: kept, nextT: missing } };
-const module: IrModule = { irVersion: 13, sourceFile: loc.file, entry: "main",
+const module: IrModule = { irVersion: 14, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [{ localId: "x", name: "x", type: missing }],
returnType: nested, locals: [], loc, body: [{ kind: "exprStmt", expr, loc }] }],
records: [{ id: "r", fields: [{ name: "callback", type: nested }], indexValue: missing }],
diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt
index 2cb7c4073..0dee3c2d6 100644
--- a/tests/harness/__snapshots__/es2025-fences.ts.txt
+++ b/tests/harness/__snapshots__/es2025-fences.ts.txt
@@ -16,32 +16,14 @@ es2025-fences.ts:15:27 - error SC2020: 'resizable ArrayBuffer construction' is p
hint: fixed-length ArrayBuffers accept a byte length; resizable storage is not supported yet
-es2025-fences.ts:18:7 - error SC2020: 'SharedArrayBuffer' is part of the standard library types but has no scriptc lowering yet
-
- 17 | // SharedArrayBuffer growth.
- 18 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 });
- | ^~~
- 19 | sab.grow(16);
-
- hint: no shared-memory threads exist in a compiled program — Uint8Array is the byte storage
-
-es2025-fences.ts:18:13 - error SC2020: 'new SharedArrayBuffer' is part of the standard library types but has no scriptc lowering yet
+es2025-fences.ts:18:13 - error SC2020: 'resizable ArrayBuffer construction' is part of the standard library types but has no scriptc lowering yet
17 | // SharedArrayBuffer growth.
18 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 });
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19 | sab.grow(16);
- hint: no shared-memory threads exist in a compiled program — Uint8Array is the byte storage
-
-es2025-fences.ts:19:1 - error SC2020: 'SharedArrayBuffer.grow' is part of the standard library types but has no scriptc lowering yet
-
- 18 | const sab = new SharedArrayBuffer(8, { maxByteLength: 16 });
- 19 | sab.grow(16);
- | ^~~~~~~~
- 20 |
-
- hint: no shared-memory threads exist in a compiled program — Uint8Array is the byte storage (a fixed-length allocation: grow has nothing to share it with)
+ hint: fixed-length ArrayBuffers accept a byte length; resizable storage is not supported yet
es2025-fences.ts:23:7 - error SC2009: values of type '{ async: false; value: "not-equal" | "timed-out"; } | { async: true; value: Promise<"ok" | "timed-out">; }' cannot be compiled: the union shape is supported, but its arm '{ async: false; value: "not-equal" | "timed-out"; }' does not compile
@@ -59,7 +41,7 @@ es2025-fences.ts:23:16 - error SC2020: 'Atomics.waitAsync' is part of the standa
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24 |
- hint: Atomics.wait(int32Array, idx, expected, timeoutMs) is the supported Atomics surface (scriptc has no threads — wait is the synchronous-sleep idiom, and nothing else has anyone to race)
+ hint: use integer load, store, exchange, compareExchange, add, sub, and, or, xor, wait or notify
es2025-fences.ts:26:7 - error SC2020: 'Float16Array' is part of the standard library types but has no scriptc lowering yet
diff --git a/tests/harness/self-hosting-llvm-emitter.test.ts b/tests/harness/self-hosting-llvm-emitter.test.ts
index a37e9254a..98fbeca26 100644
--- a/tests/harness/self-hosting-llvm-emitter.test.ts
+++ b/tests/harness/self-hosting-llvm-emitter.test.ts
@@ -14,19 +14,7 @@ import {
} from "@scriptc/compiler";
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
import { type IrModule } from "../../packages/compiler/src/ir/ir.js";
-import {
- moduleUsesAssert,
- moduleUsesBigInt,
- moduleUsesCopying,
- moduleUsesDynInvoke,
- moduleUsesEmitter,
- moduleUsesInspect,
- moduleUsesLegacyTextDecoder,
- moduleUsesRegex,
- moduleUsesStream,
- moduleUsesSymbol,
- moduleUsesZlib,
-} from "../../packages/compiler/src/ir/runtime-features.js";
+import { executableLinkFeatures } from "../../packages/compiler/src/backend/executable-features.js";
import {
llvmEmitterCases,
llvmEmitterOptions,
@@ -51,22 +39,6 @@ function programStderr(stderr: Buffer): string {
: text;
}
-function nativeFeatures(mod: IrModule) {
- return {
- regex: moduleUsesRegex(mod),
- copying: moduleUsesCopying(mod),
- inspect: moduleUsesInspect(mod),
- dynInvoke: moduleUsesDynInvoke(mod),
- symbol: moduleUsesSymbol(mod),
- bigint: moduleUsesBigInt(mod),
- zlib: moduleUsesZlib(mod),
- assert: moduleUsesAssert(mod),
- emitter: moduleUsesEmitter(mod),
- stream: moduleUsesStream(mod),
- textDecoderLegacy: moduleUsesLegacyTextDecoder(mod),
- };
-}
-
// Execute generated modules across expression dispatch, shared mutable
// emitter state, exception paths, dynamic walkers and callback adapters.
const programs = [
@@ -116,6 +88,8 @@ const programs = [
"constant-number-remainders.ts",
"numeric-call-specialization.ts",
"scalar-string-slices.ts",
+ "native-worker-roots/main.ts",
+ "native-worker-shared-views.ts",
];
for (const backend of ["llvm"] as const) {
@@ -251,7 +225,7 @@ for (const backend of ["llvm"] as const) {
outPath,
sanitize,
optimization: "dev",
- ...nativeFeatures(mod),
+ ...executableLinkFeatures(mod, false),
});
const node = spawnSync(process.execPath, [sourcePath], runOptions);
const native = spawnSync(outPath, [], runOptions);
@@ -276,7 +250,7 @@ for (const backend of ["llvm"] as const) {
outPath: secondStage,
sanitize,
optimization: "dev",
- ...nativeFeatures(ownIr),
+ ...executableLinkFeatures(ownIr, false),
});
const again = join(dir, "again.ll");
const second = await execFileAsync(secondStage, [input, again, config], {
diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts
index 6ad91af4f..5298c592f 100644
--- a/tests/harness/self-hosting-native-toolchain.test.ts
+++ b/tests/harness/self-hosting-native-toolchain.test.ts
@@ -100,7 +100,7 @@ for (const backend of ["llvm"] as const) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
- runtime_abi: { version: 6, marker: "scr_runtime_abi_v6" },
+ runtime_abi: { version: 7, marker: "scr_runtime_abi_v7" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
flavors: {
diff --git a/tests/harness/self-hosting-validator-cases.ts b/tests/harness/self-hosting-validator-cases.ts
index b61defbaa..fed9f4781 100644
--- a/tests/harness/self-hosting-validator-cases.ts
+++ b/tests/harness/self-hosting-validator-cases.ts
@@ -16,7 +16,7 @@ import { boolLit, numLit, strLit, varRef } from "../../packages/compiler/src/ir/
const loc = { file: "validator-input.ts", start: 12, end: 34 };
const expression = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
const base = (): IrModule => ({
- irVersion: 13,
+ irVersion: 14,
sourceFile: loc.file,
entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
diff --git a/tests/harness/ts7-baselines.test.ts b/tests/harness/ts7-baselines.test.ts
new file mode 100644
index 000000000..5382d5a0c
--- /dev/null
+++ b/tests/harness/ts7-baselines.test.ts
@@ -0,0 +1,55 @@
+import { spawnSync } from "node:child_process";
+import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join } from "node:path";
+import { expect, test } from "vitest";
+
+test("baseline preflight reports every missing fixture before loading the compiler", () => {
+ const root = mkdtempSync(join(tmpdir(), "scriptc-baseline-preflight-"));
+ const put = (path: string, content: string) => {
+ const fullPath = join(root, path);
+ mkdirSync(dirname(fullPath), { recursive: true });
+ writeFileSync(fullPath, content);
+ };
+ try {
+ mkdirSync(join(root, "scripts"));
+ for (const name of ["test-ts7.mjs", "ts7-order-fixtures.mjs"]) {
+ copyFileSync(join(import.meta.dirname, "../../scripts", name), join(root, "scripts", name));
+ }
+ const fixtures = [
+ "tests/corpus/plain.ts",
+ "tests/corpus/common.cjs",
+ "tests/corpus/modules/main.mjs",
+ "tests/diagnostics/refused.ts",
+ "tests/fixtures/npm/cases/example/main.js",
+ "tests/fixtures/strictness/strict/main.ts",
+ "tests/fixtures/node-types/api.ts",
+ ];
+ for (const path of [...fixtures, "tests/corpus/modules/helper.ts"]) put(path, "");
+ const baselinePath = "packages/compiler/test/ts7/baselines/order-parity.json";
+ put(baselinePath, JSON.stringify({ entries: {} }));
+ const run = () =>
+ spawnSync(process.execPath, [join(root, "scripts/test-ts7.mjs"), "--baselines-only"], {
+ encoding: "utf8",
+ });
+ const missing = run();
+ expect(missing.status).toBe(1);
+ for (const path of fixtures) expect(missing.stderr).toContain(path);
+ expect(missing.stderr).not.toContain("helper.ts");
+ expect(missing.stderr).toContain("SCRIPTC_UPDATE_BASELINES=1");
+
+ put(
+ baselinePath,
+ JSON.stringify({
+ entries: Object.fromEntries(
+ fixtures.map((path) => [`/${path}`, { order: [], diags: [] }]),
+ ),
+ }),
+ );
+ const complete = run();
+ expect(complete.status, complete.stderr).toBe(0);
+ expect(complete.stdout).toContain("Every TypeScript preflight/order fixture");
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});