diff --git a/.gitignore b/.gitignore
index f747b4f62..e5b54a5fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ node_modules/
.pnpm-store/
# test-fixture packages are TEST DATA, committed on purpose
!tests/diagnostics/npm-imports/node_modules/
+!tests/diagnostics/npm-program-type-error/node_modules/
!tests/diagnostics/retired-engine-import-refusal/node_modules/
!tests/fixtures/commander-calc/node_modules/
!tests/fixtures/npm/node_modules/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f191367ca..3de3f334f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,40 @@ All notable changes to scriptc will be documented in this file.
+## 0.3.6
+
+### Features
+
+- **`node:readline` adds terminal mode, cursor helpers and `emitKeypressEvents`,** along with the `history` and `completer` options and the history members.
+- **`node:http` supports request `signal`s, `destroy(err)`, `servername`, `Expect` events and `maxHeaderSize`.**
+- **`createHash` and `createHmac` values work as streams.**
+- **`node:test` adds the `concurrency`, `plan` and `timeout` options.**
+- **`fs.cpSync`, `fs.cp` and `fs/promises` `cp` are supported.**
+- **`util.promisify` works over `setTimeout`, `setImmediate`, `stream.finished`, `stream.pipeline` and the zlib callback one-shots.**
+- **`Array.fromAsync` works over arrays,** `Readable.from` accepts Sets, iterators and class iterables, and the `Set` composition methods accept `Map` arguments.
+- **`using` disposes `Disposable`-typed values,** and `SuppressedError` exposes `error` and `suppressed`.
+
+### Performance
+
+- **Int32 class fields stay int32 next to arithmetic on dynamic operands** and in programs that register process event listeners. A large TypeScript type-checker workload runs about 12% faster with about 5% less memory.
+- **`while` loops bounded by a string's length keep int32 counters.**
+- **Worker threads reserve heap growth up front** once they allocate many large blocks, and share one prefault budget.
+- **Small runtime functions are inlined into release executables.**
+- **Integers are speculated only where the static range proof gives up.**
+- **Constant-key dynamic reads are cached per site,** and `JSON.parse` follows repeated key sequences.
+- **The zlib one-shot forms run on the stream engine,** and `statSync`'s `throwIfNoEntry` option no longer builds the error it discards.
+
+### Fixes
+
+- **Class instances keep their identity through structural record views,** including in `switch` statements and assertions.
+- **Node 24 builtin forms that ran with wrong output now match Node or are refused,** and `Object.groupBy` results have a null prototype.
+- **IPv4 hosts in special-scheme URLs parse like WHATWG URL.**
+- **fs paths with a null byte are refused as Node does.**
+- **`Object.setPrototypeOf` returns primitive receivers,** and `createHmac` without a key throws Node's error.
+- **Boxed records that hold an async function link the dynamic promise unit.**
+
+
+
## 0.3.5
### Features
@@ -36,8 +70,6 @@ All notable changes to scriptc will be documented in this file.
- **Publication walkers with more than ten pushes keep distinct value names,** and int32 field storage survives reflection reads through `unknown`.
- **Native program objects use runtime ABI v22.** External object consumers must link the matching v0.3.5 runtime pack; the versioned marker rejects incompatible runtime objects at link time.
-
-
## 0.3.4
### Features
diff --git a/benchmarks/scaling/arrays.ts b/benchmarks/scaling/arrays.ts
index 1e731bc67..3e907d358 100644
--- a/benchmarks/scaling/arrays.ts
+++ b/benchmarks/scaling/arrays.ts
@@ -40,6 +40,36 @@ function queue(n: number): string {
}
return `${s} ${q.length}`;
}
+function spliceFront(n: number): string {
+ const a = make(n);
+ let s = 0;
+ while (a.length > 0) s += a.splice(0, 1)[0]!;
+ return `${s}`;
+}
+function queueGrow(n: number): string {
+ // BFS over a binary tree: the frontier grows to ~n/2 entries.
+ const q: number[] = [1];
+ let s = 0;
+ let seen = 1;
+ while (q.length > 0) {
+ const v = q.shift()!;
+ s += v;
+ if (seen < n) { q.push(v * 2); seen++; }
+ if (seen < n) { q.push(v * 2 + 1); seen++; }
+ }
+ return `${s % 1000000007} ${seen}`;
+}
+function deque(n: number): string {
+ // Values enter and leave at both ends; ~n/3 stay live.
+ const d: number[] = [];
+ let s = 0;
+ for (let i = 0; i < n; i++) {
+ if (i & 1) d.unshift(i);
+ else d.push(i);
+ if (i % 3 === 0) s += d.shift()!;
+ }
+ return `${s} ${d.length} ${d[0]} ${d[d.length - 1]}`;
+}
function unshift(n: number): string {
const a: number[] = [];
for (let i = 0; i < n; i++) a.unshift(i);
@@ -216,6 +246,9 @@ switch (which) {
case "shift": out = shift(n); break;
case "queue": out = queue(n); break;
case "unshift": out = unshift(n); break;
+ case "splice-front": out = spliceFront(n); break;
+ case "queue-grow": out = queueGrow(n); break;
+ case "deque": out = deque(n); break;
case "splice-tail": out = spliceTail(n); break;
case "splice-mid": out = spliceMid(n); break;
case "slice-small": out = sliceSmall(n); break;
diff --git a/benchmarks/scaling/run.mjs b/benchmarks/scaling/run.mjs
index 907ac4d69..3bd68e949 100644
--- a/benchmarks/scaling/run.mjs
+++ b/benchmarks/scaling/run.mjs
@@ -35,7 +35,8 @@ const SQ = [250, 500, 1000, 2000];
// [file, case, sizes]
const CASES = [
...[
- "push", "pop", "shift", "queue", "splice-tail", "slice-small", "concat", "indexOf",
+ "push", "pop", "shift", "queue", "queue-grow", "splice-front", "deque", "splice-tail",
+ "slice-small", "concat", "indexOf",
"includes-str", "sort-num", "sort-sorted", "sort-str", "sort-obj", "reverse", "in-arr",
"in-arr-str", "holes", "sparse", "spread", "map-filter-reduce", "array-from", "join",
"flat", "lastIndexOf", "findIndex", "nested2d", "length-truncate",
diff --git a/docs/content/docs/contracts.mdx b/docs/content/docs/contracts.mdx
index cb0a3f75d..d1bfb8cad 100644
--- a/docs/content/docs/contracts.mdx
+++ b/docs/content/docs/contracts.mdx
@@ -25,6 +25,11 @@ Contracts are type aliases imported from `@scriptc/types` with `import type`. Ty
Every value stored is a signed 32-bit integer by construction. |
The variable, field, parameter or result is stored as a 32-bit integer. |
+
+ Int32[] |
+ Every element stored into the array, through any reference, is a signed 32-bit integer by construction. |
+ Integer arithmetic over its elements runs in 32-bit integer operations. |
+
Immutable<T> |
No code writes to the value, through any reference. |
@@ -120,6 +125,8 @@ scriptc recognizes a contract where it is written as the annotation of a declara
The written return type of a function, method or arrow function. NoThrow<Int32> and NoThrow<Immutable<T>> combine the result contracts.
Int32
Parameters, variables, class instance fields and function results whose type is a plain number.
+ Int32[]
+ Parameters, variables, class instance fields and function results whose type is a plain number[], written Int32[], readonly Int32[], Array<Int32> or ReadonlyArray<Int32>.
Immutable<T>
Parameters, variables, class instance fields and function results.
Acyclic
@@ -128,7 +135,7 @@ scriptc recognizes a contract where it is written as the annotation of a declara
The implements clause of a class declaration: class Node implements ProgramLifetime.
-A contract anywhere else (a type alias, an interface member, an element or type argument such as `Int32[]`, a function type) would promise something scriptc does not check, so the build reports it with SC7006.
+A contract anywhere else (a type alias, an interface member, a nested array or other type argument such as `Int32[][]` or `Map`, a function type) would promise something scriptc does not check, so the build reports it with SC7006.
## NoThrow
@@ -178,6 +185,68 @@ Each value stored into an `Int32` slot (initializers, assignments, compound assi
scriptc refuses an `Int32` slot whose writes it cannot all see (SC7003): a parameter of a function used as a value, or a field of a class whose instances are stored in `any` or `unknown` values.
+## Int32 arrays
+
+`Int32[]` promises that every element stored into the array is an int32, so an element read that finds a value reads an int32. Integer kernels over such arrays, such as sums, hashes and prefix sums written with `| 0` or `Math.imul`, then compute in 32-bit integer operations instead of converting every element and intermediate result between doubles and integers:
+
+```ts:checksum.ts
+import type { Int32 } from "@scriptc/types";
+
+function checksum(data: Int32[]): number {
+ let h = 17;
+ for (let i = 0; i < data.length; i++) h = (Math.imul(h, 31) + data[i]) | 0;
+ return h;
+}
+
+const samples: Int32[] = [];
+for (let i = 0; i < 1000; i++) samples.push((i * 7919) & 0xffff);
+console.log(checksum(samples));
+samples[1200] = -1;
+console.log(samples.length, samples[1100], checksum(samples));
+```
+
+```console
+$ node checksum.ts
+-220195363
+1201 undefined -1
+$ scriptc build checksum.ts -o checksum >/dev/null
+$ ./checksum
+-220195363
+1201 undefined -1
+```
+
+The contract constrains the values stored, not the array's shape. Holes, present `undefined` and indices past the end read as `undefined`, and as `NaN` in arithmetic, exactly as in Node.js; writing past the end or changing `length` behaves as usual. The elements stay ordinary numbers, so `a[i] + a[j]` can exceed the int32 range.
+
+The promise belongs to the array, not to one variable, so scriptc checks every store into any array that can reach an `Int32[]` slot, through any reference and wherever it happens, including before the array reaches the slot: `a[i] = v`, `push`, `unshift`, `fill`, `with`, `splice` items, array literal elements, and elements copied in by spreads and `concat`. Each stored value must be an int32 in the same sense as for `Int32`, and an element read from another `Int32[]` array counts as one. Arrays flow through parameters, results, variables, class fields, object properties, `Map` values, nested arrays and captured bindings; reads through any of them are faster when only checked arrays can reach them, so the annotation is needed only where the program declares the array:
+
+```ts:scale.ts
+import type { Int32 } from "@scriptc/types";
+
+function scale(values: number[], factor: number): void {
+ for (let i = 0; i < values.length; i++) values[i] = values[i] * factor;
+}
+
+const counts: Int32[] = [3, 5, 8];
+scale(counts, 1.5);
+console.log(counts);
+```
+
+```console
+$ scriptc build scale.ts
+/tmp/app/scale.ts:4:55 - error SC7002: the element stored into 'values' is not provably an int32: 'values[i] * factor' (the array can reach 'counts', declared Int32[])
+
+ 3 | function scale(values: number[], factor: number): void {
+ 4 | for (let i = 0; i < values.length; i++) values[i] = values[i] * factor;
+ | ^~~~~~~~~~~~~~~~~~
+ 5 | }
+
+ hint: make the int32 conversion explicit, for example '(values[i] * factor) | 0', so Node stores the same value
+
+1 error.
+```
+
+A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of callbacks are not followed, so an array built by `map` or `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
+
## Immutable
`Immutable` is a deep read-only view, so TypeScript already rejects direct writes such as `KEYWORDS.set(...)`. scriptc also checks the writes TypeScript cannot see:
@@ -262,7 +331,7 @@ $ scriptc build graph.ts
## ProgramLifetime
-`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed.
+`ProgramLifetime` marks a class whose instances a program keeps until it exits, such as the nodes of a syntax tree, the symbols of a symbol table or interned types. scriptc then never reference counts, collects or frees instances of the class and of its subclasses: they are never freed, every retain and release on them compiles to nothing, and the cycle collector neither walks them nor needs to trace the objects that point to them. Node.js behavior is unchanged; only memory use differs, because an instance the program drops is never reclaimed. On Linux, the instances a worker thread creates are returned with the rest of its memory after it exits, unless the worker shared objects with other threads (see [cost](/docs/threads#cost)).
```ts:tree.ts
import type { ProgramLifetime } from "@scriptc/types";
@@ -371,11 +440,11 @@ $ scriptc build reject.ts
| SC7002 |
- A value stored into an Int32 slot is not provably an int32; the hint shows the | 0 form. |
+ A value stored into an Int32 slot, or an element stored into an array that can reach an Int32[] slot, is not provably an int32; the hint shows the | 0 form. |
| SC7003 |
- An Int32 slot has writes scriptc cannot see. |
+ An Int32 slot has writes scriptc cannot see, or an Int32[] array can come from or escape to code scriptc cannot see. |
| SC7004 |
diff --git a/docs/content/docs/how-it-works.mdx b/docs/content/docs/how-it-works.mdx
index 4283a089c..5c6d7be63 100644
--- a/docs/content/docs/how-it-works.mdx
+++ b/docs/content/docs/how-it-works.mdx
@@ -25,7 +25,7 @@ LLVM helper -------------> assembly / object (--emit=asm|obj)
Runtime pack and linker -> executable
```
-1. **Frontend:** TypeScript parses and checks the program against `es2025`, scriptc's ambient declarations, and available project declarations. `tsconfig.json` controls checker strictness. The compiler uses the checked types and narrowing information to lower supported operations; unsupported operations produce diagnostics.
+1. **Frontend:** TypeScript parses and checks the program against `es2025`, scriptc's ambient declarations, and available project declarations. `tsconfig.json` controls checker strictness. The compiler uses the checked types and narrowing information to lower supported operations; unsupported operations produce diagnostics. JavaScript files that TypeScript checks (a `// @ts-check` comment, or `checkJs` in `tsconfig.json`) report values that do not match their JSDoc types as errors, and their classes use those types the way TypeScript classes use annotations.
2. **Typed IR:** A validated, serializable intermediate representation connects the frontend and backend. Types are concrete: generic functions are specialized, unions use tags, and closures have explicit captures. `--emit=ir` writes this representation as JSON.
3. **LLVM backend:** `--emit=llvm` writes textual LLVM IR. The matching LLVM 22 helper generates assembly or objects on supported macOS, Linux, and Windows hosts, including output targeting WASI. Executable and library builds use this backend. Large executables, native and WASI alike, are divided into partitions that are optimized and compiled in parallel; optimized builds summarize every partition first so small functions can still be inlined across partitions. The partition count depends only on the program, so output is the same on every host, and a rebuild reuses the compiled partitions that an edit does not affect.
4. **Linking:** Runtime packs contain the precompiled C runtime and vendored libraries. A hashed manifest selects the artifacts the program needs for its features, optimization mode, and (for libraries) state model. The target linker and SDK or sysroot produce the executable. Sanitizer builds use an instrumented C runtime.
@@ -77,7 +77,7 @@ Program objects define `main` and leave selected `scr_*` runtime functions undef
## Native runtime
- **Memory:** Values use reference counting. Acyclic values are freed when their last reference is released, and a cycle collector runs at defined collection points. Some cycles involving opaque native values can remain allocated. When the main thread's script ends, the process exits without freeing what the program still holds, and the operating system reclaims it; worker threads free their heaps when they exit. Builds with `--sanitize`, and processes run with `SCRIPTC_EXIT_FREE=1` (for external leak checkers), free everything at exit.
-- **Concurrency:** `async`/`await` uses stackful fibers. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux.
+- **Concurrency:** An `async` function that suspends only at its own `await` expressions compiles to a stackless coroutine whose heap frame holds the values live across those awaits; other suspending `async` functions and async generators use stackful fibers. The native event loop schedules microtasks, timers, and I/O, using kqueue on macOS and epoll on Linux.
- **Networking:** Supported `net`, `http`, `https`, `tls`, `dgram`, and `dns` APIs use native implementations. TLS uses vendored mbedTLS.
- **Numbers:** Values use JavaScript's double-precision representation. Number-to-string formatting uses shortest-roundtrip output and is tested against Node.js.
- **Regular expressions and Unicode:** The runtime uses QuickJS's regular-expression and Unicode libraries, linked into programs that use them.
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index 420af8200..ed2c5b1ec 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -208,6 +208,10 @@ title: "Limitations"
../limitations/dns-lookup-errors.mdx
+### Socket data encoding
+
+../limitations/socket-data-encoding.mdx
+
### UDP socket options
../limitations/udp-socket-options.mdx
diff --git a/docs/content/docs/platforms.mdx b/docs/content/docs/platforms.mdx
index b4f817670..96b91fc2a 100644
--- a/docs/content/docs/platforms.mdx
+++ b/docs/content/docs/platforms.mdx
@@ -69,9 +69,12 @@ Executables open a console window by default. For an application that shows its
### WebAssembly (WASI)
+WASI is a cross-compile target, so it needs [zig](https://ziglang.org) and the `@scriptc/runtime-wasm32-wasi` runtime pack, as described in [Cross-compile](#cross-compilation-via-zig). Install the runtime pack in your project at the same version as `scriptc --version`. Without it, the build fails with `SC3003`.
+
Set `SCRIPTC_TARGET=wasm32-wasi` to build a standalone `.wasm` module:
```console
+$ npm install --save-dev @scriptc/runtime-wasm32-wasi@0.3.5
$ SCRIPTC_TARGET=wasm32-wasi scriptc build hello.ts >/dev/null
$ SCRIPTC_TARGET=wasm32-wasi scriptc run hello.ts
hello, world
diff --git a/docs/content/docs/threads.mdx b/docs/content/docs/threads.mdx
index 7b394e09f..6f9b5f062 100644
--- a/docs/content/docs/threads.mdx
+++ b/docs/content/docs/threads.mdx
@@ -234,6 +234,8 @@ Without `@scriptc/threads`, a scriptc executable also shares a message of untype
Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. `SCRIPTC_WORKER_STATS=1` prints, as each worker exits, how long it took to start, to run its script and to exit.
+On Linux, a worker that exits without having shared an object it allocated with another thread returns all of its memory, including objects it never freed, such as `ProgramLifetime` instances, once no other worker thread is running. A worker shares objects when, for example, it sends a published value or frozen plain data, which other threads read by reference, or stores a value in an `InternTable`. The memory of a worker that shared stays allocated, as on other platforms. Set `SCR_SA_DISCARD=0` in the environment to keep the memory of every worker.
+
`publish` visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch.
Threads that add to one shared collection of published values while they run can use an [`InternTable`](/docs/experimental#interntable) from `@scriptc/experimental`.
diff --git a/docs/content/docs/wasm.mdx b/docs/content/docs/wasm.mdx
index dd3862cd2..b498eccf7 100644
--- a/docs/content/docs/wasm.mdx
+++ b/docs/content/docs/wasm.mdx
@@ -6,6 +6,12 @@ A WebAssembly module lets a web page or another application call functions writt
You build a module in library mode, with `SCRIPTC_TARGET=wasm32-wasi` and `scriptc build --lib --profile`. The result is a WASI Preview 1 reactor: a `.wasm` file with callable exports, named imports for host callbacks, and its own memory. Your application loads the module, supplies its imports, calls its exports, and handles events, scheduling, and rendering.
+Module builds need [zig](https://ziglang.org) and the `@scriptc/runtime-wasm32-wasi` runtime pack in your project, at the same version as `scriptc --version`. See [Cross-compile](/docs/platforms#cross-compilation-via-zig).
+
+```console
+$ npm install --save-dev @scriptc/runtime-wasm32-wasi@0.3.5
+```
+
## Define the interface
The library profile names the entry module and its public functions. A reachable callback call becomes a synchronous import from the scriptc namespace.
@@ -90,11 +96,13 @@ After initialization, counter_step(2) returns 2, and a
i64, u64 | JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range. |
string, bytes parameters | Two i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values. |
string, bytes results | Two extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order. |
+ cstring parameters | One i32 argument: the byte offset of NUL-terminated UTF-8 text. The text is copied into a runtime-owned string; malformed UTF-8 becomes U+FFFD. A zero pointer traps with SC4012. |
+ cstring results | An i32 result: the byte offset of NUL-terminated UTF-8 text in the result arena. A result containing U+0000 traps with SC4015 instead of being truncated. |
void | No result. |
-The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host.
+The native library rules about directions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host. A cstring callback parameter is one i32 pointer to NUL-terminated text, borrowed the same way; a value containing U+0000 traps with SC4015. The cstring class requires "profile_format": 2. In native libraries it is a const char * with the same rules.
Allocate host input buffers and output slots with scriptc_alloc(size); a zero result indicates allocation failure. Release each allocation exactly once with scriptc_free(pointer). Only free pointers returned by scriptc_alloc. These allocations remain live across application calls, initialization, and collection until freed.
diff --git a/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx b/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx
index 81dfded25..59a61518d 100644
--- a/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx
+++ b/docs/content/limitations/builtin-apis/arraybuffers-and-typed-arrays.mdx
@@ -1 +1 @@
-- **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. `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does. Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's. Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool.
+- **ArrayBuffers and typed arrays:** Fixed-length ArrayBuffers support shared typed-array and DataView storage. Resizing, transferring, and BigInt arrays are unsupported. `Float16Array` stores IEEE 754 half-precision values, rounding each stored number to nearest-even as Node does; `Math.f16round`, `DataView.prototype.getFloat16` and `setFloat16`, and `util.types.isFloat16Array` compile. 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. `Buffer.from`, `Buffer.allocUnsafe`, and `Buffer.concat` return views into a per-thread pool of `Buffer.poolSize` bytes (64 KiB by default) when the size is below `Buffer.poolSize >>> 1`, as in Node; a pool slab stays in memory while any of its Buffers does. Node's own startup code allocates from the first slab, so the `byteOffset` of Buffers in that slab differs from Node's. Buffers returned by other Node APIs, such as `fs.readFileSync` and `zlib`, own their `ArrayBuffer`, where Node allocates small ones from the pool.
diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx
index 5c465b315..db5278c53 100644
--- a/docs/content/limitations/builtin-apis/date.mdx
+++ b/docs/content/limitations/builtin-apis/date.mdx
@@ -1 +1 @@
-- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
+- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
diff --git a/docs/content/limitations/builtin-apis/exit-status.mdx b/docs/content/limitations/builtin-apis/exit-status.mdx
index e2b681daf..f13782ff2 100644
--- a/docs/content/limitations/builtin-apis/exit-status.mdx
+++ b/docs/content/limitations/builtin-apis/exit-status.mdx
@@ -1 +1 @@
-- **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.
+- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument. `process.exit(code)` and `process.exitCode` throw a `RangeError` (`ERR_OUT_OF_RANGE`) for a code that is not a safe integer, and convert other codes to int32, as Node does. Reads, resets, and assignments used as values are unsupported.
diff --git a/docs/content/limitations/builtin-apis/regular-expressions.mdx b/docs/content/limitations/builtin-apis/regular-expressions.mdx
index 4578cfc93..a765bbe3c 100644
--- a/docs/content/limitations/builtin-apis/regular-expressions.mdx
+++ b/docs/content/limitations/builtin-apis/regular-expressions.mdx
@@ -1 +1 @@
-- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `d` (match indices) and `v` (unicode sets) flags are unsupported: regex literals and `new RegExp` calls with a literal flag string containing them fail to compile with SC1120, and `new RegExp` with a computed flag string containing them throws an error naming the flag.
+- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
diff --git a/docs/content/limitations/builtin-apis/test-mocks.mdx b/docs/content/limitations/builtin-apis/test-mocks.mdx
index c9e84a80d..a0c63cd98 100644
--- a/docs/content/limitations/builtin-apis/test-mocks.mdx
+++ b/docs/content/limitations/builtin-apis/test-mocks.mdx
@@ -1 +1,2 @@
-- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module`, `mock.timers` and mock options throw `SC2020`.
+- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
+- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. As in Node, the global timer functions, `Date.now()`, `new Date()` and `scheduler.wait` are mocked, and other calls through named and namespace imports of `node:timers` and `node:timers/promises` use real timers. As in Node, once a reset gives `scheduler.wait` back, calling it throws `ERR_INVALID_THIS`. While `scheduler.wait` is mocked, calling it with a `signal` option throws an `SC2020` error. A mock timer's handle converts to a negative number, where Node's mock `Timeout` converts to `NaN`. When two trackers have timers enabled, clearing a timer through the tracker that did not create it leaves the timer queued, where Node removes the entry at the same queue position in the clearing tracker.
diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx
index be138b3ca..7a50901db 100644
--- a/docs/content/limitations/builtin-apis/test-runner.mdx
+++ b/docs/content/limitations/builtin-apis/test-runner.mdx
@@ -1 +1,2 @@
- **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node. Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites. Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node.
+- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`.
diff --git a/docs/content/limitations/contract-types.mdx b/docs/content/limitations/contract-types.mdx
index 59bb28287..977150259 100644
--- a/docs/content/limitations/contract-types.mdx
+++ b/docs/content/limitations/contract-types.mdx
@@ -1 +1 @@
-[Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported. In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`. `Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values, and `Immutable` refuses values stored in `any` or `unknown` or that another reference can still change. `ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion).
+[Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported. In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`. `Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values. `Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, does not follow callback results (such as `map` results), and checks a copy made by `slice` or a spread as if it were its source array. Element reads that find a value are int32s, but a scalar `Int32` slot still needs `a[i] | 0`, because a read can miss and produce `undefined`. `Immutable` refuses values stored in `any` or `unknown` or that another reference can still change. `ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion).
diff --git a/docs/content/limitations/language-features/async-iteration.mdx b/docs/content/limitations/language-features/async-iteration.mdx
index ee570f17d..f18377299 100644
--- a/docs/content/limitations/language-features/async-iteration.mdx
+++ b/docs/content/limitations/language-features/async-iteration.mdx
@@ -1 +1 @@
-- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises with an optional mapping function; async generator sources, a `thisArg`, and arrays that mix values and promises are unsupported.
+- **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported.
diff --git a/docs/content/limitations/language-features/resource-disposal.mdx b/docs/content/limitations/language-features/resource-disposal.mdx
index 5163d5322..d3df4138e 100644
--- a/docs/content/limitations/language-features/resource-disposal.mdx
+++ b/docs/content/limitations/language-features/resource-disposal.mdx
@@ -1 +1 @@
-- **Resource disposal:** Top-level and switch-clause `using` declarations, and `DisposableStack` constructors are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`).
+- **Resource disposal:** Top-level and switch-clause `using` declarations, and `AsyncDisposableStack`, are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first). Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws.
diff --git a/docs/content/limitations/native-worker-threads.mdx b/docs/content/limitations/native-worker-threads.mdx
index 0527c24b0..0510936d6 100644
--- a/docs/content/limitations/native-worker-threads.mdx
+++ b/docs/content/limitations/native-worker-threads.mdx
@@ -1,6 +1,6 @@
-`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, and a `transferList` of MessagePorts; runtime paths, `eval`, loaders, custom stdio, shared environments, and resource limits are unsupported. Worker argument strings containing null bytes are also unsupported.
+`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB). The `resourceLimits` heap limits (`maxOldGenerationSizeMb`, `maxYoungGenerationSizeMb`, and `codeRangeSizeMb`), `worker.resourceLimits` while the worker runs, and `resourceLimits` read inside a worker are refused; in the main thread `resourceLimits` is `{}`, and after a worker exits `worker.resourceLimits` is `{}`, as in Node. With the `stdout` or `stderr` option, the worker's writes to that stream reach `worker.stdout` or `worker.stderr`, a Readable that ends after the worker's `'exit'` event; its chunks arrive in order with the worker's other events. Without the option a worker writes to the process's own output, and reading `worker.stdout` or `worker.stderr` is refused. With the `stdin` option, `worker.stdin` is a Writable whose chunks the worker reads from `process.stdin`, in order with the worker's other events, and `process.stdin` ends when `worker.stdin` ends; without it `worker.stdin` is `null` and the worker's `process.stdin` ends without data, as in Node. Runtime paths, `eval`, loaders, and shared environments are unsupported. `setEnvironmentData` and `getEnvironmentData` keep a map per thread with SameValueZero keys; each new Worker starts from a clone of its parent's entries taken by the constructor, which throws `DataCloneError` when an entry cannot be cloned. Worker argument strings containing null bytes are also unsupported.
-Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. A port's `onmessage` property, `addEventListener`, `receiveMessageOnPort(parentPort)`, and `BroadcastChannel` are unsupported. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).
+Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `worker[Symbol.asyncDispose]()` terminates the worker and resolves with `undefined` after its `exit` event, so `await using` disposes a Worker at the end of its block. A worker's uncaught exception reaches its `error` listeners as Node's error serializer sends it: an Error keeps the nearest of `Error` and its six native subclasses (an `AggregateError` arrives as an `Error` named `AggregateError`) along with its name, message, cause, and other data properties and their enumerability, and other thrown values clone as messages do. An unhandled rejection whose reason is not error-like arrives as an `UnhandledPromiseRejection` Error with code `ERR_UNHANDLED_REJECTION` quoting the reason. A thrown value that does not clone, such as a function, arrives as an `ERR_WORKER_UNSERIALIZABLE_ERROR` Error, where Node sends its `util.inspect` string. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. `receiveMessageOnPort(parentPort)` in a worker takes the parent's next message. A port's `onmessage` property and `addEventListener` are unsupported. `BroadcastChannel` delivers each message to every other open channel with the same name, in any thread, as a `'message'` `MessageEvent` through `onmessage` and `addEventListener`; an open channel keeps the process alive until `close()` or `unref()`. A `messageerror` event is never dispatched. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. `markAsUntransferable` marks objects for the current thread, and a marked ArrayBuffer or MessagePort in a transfer list throws `DataCloneError`, as does the ArrayBuffer of a pooled Buffer; marked objects other than ArrayBuffers stay alive until their thread exits. `markAsUncloneable` marks a plain object, an error, or a class instance, and `structuredClone` and `postMessage` then throw `DataCloneError` for it, also where it is nested; as in Node, the mark has no effect on primitives, arrays, typed arrays, and ArrayBuffers. A plain object that the compiler copies into a container with a fixed element type, such as an array literal whose elements all have one shape, is a separate object in the copy, which is not marked. Marked objects stay alive until their thread exits. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).
SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. Atomics operations run as sequentially consistent atomic instructions and take no lock. Other reads and writes of shared storage access each element atomically; a copy, fill, or conversion that runs while another thread writes the same storage can observe some of those writes, as in Node. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint.
diff --git a/docs/content/limitations/process-arguments-and-errors.mdx b/docs/content/limitations/process-arguments-and-errors.mdx
index 4cca7a808..d048b4154 100644
--- a/docs/content/limitations/process-arguments-and-errors.mdx
+++ b/docs/content/limitations/process-arguments-and-errors.mdx
@@ -1,4 +1,4 @@
-`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
+`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
Uncaught exceptions print `Uncaught ` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
diff --git a/docs/content/limitations/runtime-type-checks.mdx b/docs/content/limitations/runtime-type-checks.mdx
index 5514fb41e..ee7b953ce 100644
--- a/docs/content/limitations/runtime-type-checks.mdx
+++ b/docs/content/limitations/runtime-type-checks.mdx
@@ -3,3 +3,5 @@ Casts from values whose type is only known at runtime validate the value. For ex
Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. `SC6004` reports calls that pass such an instance to a function that casts it.
A Promise converted from `unknown` to a typed result validates its fulfilled value. A mismatch rejects with a `TypeError`. Rejection reasons and reference equality are preserved across conversions.
+
+A non-null assertion (`value!`) checks the value when it extracts an object, array or other non-nullable member from a union. If the value holds `null` or `undefined`, the assertion throws a `TypeError`, even when the result is never used; Node.js continues until a property of the value is read.
diff --git a/docs/content/limitations/socket-data-encoding.mdx b/docs/content/limitations/socket-data-encoding.mdx
new file mode 100644
index 000000000..8094f8143
--- /dev/null
+++ b/docs/content/limitations/socket-data-encoding.mdx
@@ -0,0 +1 @@
+After `socket.setEncoding("utf8")`, an inline `'data'` listener receives strings, as in Node, whatever type its chunk parameter declares. A listener passed by reference, such as a named function whose parameter is typed `Buffer`, keeps its `Buffer` parameter, so a string chunk throws `TypeError: expected Uint8Array at $[0], got string`. Declare that parameter as `string` or `string | Buffer`, or pass an inline function.
diff --git a/docs/content/limitations/type-checking.mdx b/docs/content/limitations/type-checking.mdx
index e12c3afb4..5af0361c6 100644
--- a/docs/content/limitations/type-checking.mdx
+++ b/docs/content/limitations/type-checking.mdx
@@ -1,3 +1,3 @@
-scriptc checks programs against the `es2025` standard library and its own ambient declarations. Some declarations differ from TypeScript's standard library or `@types/node`: `JSON.parse` returns `unknown`, and a Promise executor's reject reason is typed as `Error`.
+scriptc checks programs against the `es2025` standard library, the ESNext declarations for `Array.fromAsync`, explicit resource management and `Error.isError`, and its own ambient declarations. Some declarations differ from TypeScript's standard library or `@types/node`: `JSON.parse` returns `unknown`, and a Promise executor's reject reason is typed as `Error`.
A program that passes its project's TypeScript check may still use unsupported operations. A second preflight using the project's declarations distinguishes those sites from ordinary type errors and reports scriptc diagnostics with rewrite hints. Declared APIs without a compiler implementation, such as unsupported console members, produce `SC2020`.
diff --git a/docs/content/limitations/untyped-values.mdx b/docs/content/limitations/untyped-values.mdx
index f0ab9a526..c0811a51e 100644
--- a/docs/content/limitations/untyped-values.mdx
+++ b/docs/content/limitations/untyped-values.mdx
@@ -1,6 +1,6 @@
Supported uses of `any` use native checked values. Operations without native lowering produce diagnostics such as `SC2011`. Use `unknown` with a checked cast where an operation needs a concrete type. Map and Set type arguments may use `any`, with the same checked storage as `unknown`.
-Locals, parameters, and returns can carry untyped values; record and tuple fields can hold `unknown`. JavaScript instance fields can also hold native checked values. Bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order. Explicit TypeScript `any`/`unknown` class fields, static checked-value fields, statically typed array elements, and union members are unsupported.
+Locals, parameters, and returns can carry untyped values; record and tuple fields can hold `unknown`. JavaScript instance fields and TypeScript class fields declared `unknown` or `object` (also with `null` or `undefined`, or as arrays of those) can also hold native checked values. Bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order. Explicit TypeScript `any` class fields, other checked-value static fields, statically typed array elements, and union members are unsupported.
Program class instances retain identity through `unknown` and casts back to their exact class or a base class, including classes with internal collections. Inspecting dynamic properties throws a catchable `TypeError` if the class's fields cannot pass through checked storage. Supported native handles also retain identity. JavaScript argument and return conversions preserve native array references; other JSON-shaped record and array conversions can copy values.
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index d6311fdf7..db36cdb52 100644
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ b/docs/src/generated/node-v24-compatibility-meta.json
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
- "artifactVersion": "76b4947b60f682a7ab02",
+ "artifactVersion": "cade1b8e6e0c5c42fcc9",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index 740dffeca..dadc9e836 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -299,9 +299,9 @@
"apiEntries": 78,
"static": {
"supported": 18,
- "partial": 34,
+ "partial": 35,
"refused": 0,
- "not-implemented": 26,
+ "not-implemented": 25,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -493,10 +493,10 @@
"entries": 119,
"apiEntries": 119,
"static": {
- "supported": 0,
- "partial": 37,
+ "supported": 16,
+ "partial": 39,
"refused": 0,
- "not-implemented": 82,
+ "not-implemented": 64,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -509,9 +509,9 @@
"apiEntries": 24,
"static": {
"supported": 0,
- "partial": 23,
+ "partial": 24,
"refused": 0,
- "not-implemented": 1,
+ "not-implemented": 0,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -569,9 +569,9 @@
"apiEntries": 120,
"static": {
"supported": 0,
- "partial": 50,
+ "partial": 51,
"refused": 0,
- "not-implemented": 70,
+ "not-implemented": 69,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -689,9 +689,9 @@
"apiEntries": 117,
"static": {
"supported": 0,
- "partial": 45,
+ "partial": 53,
"refused": 0,
- "not-implemented": 72,
+ "not-implemented": 64,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -704,9 +704,9 @@
"apiEntries": 25,
"static": {
"supported": 0,
- "partial": 20,
+ "partial": 22,
"refused": 0,
- "not-implemented": 5,
+ "not-implemented": 3,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -779,9 +779,9 @@
"apiEntries": 62,
"static": {
"supported": 0,
- "partial": 39,
+ "partial": 42,
"refused": 0,
- "not-implemented": 23,
+ "not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -794,9 +794,9 @@
"apiEntries": 109,
"static": {
"supported": 0,
- "partial": 53,
+ "partial": 72,
"refused": 0,
- "not-implemented": 56,
+ "not-implemented": 37,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -884,9 +884,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
- "partial": 26,
+ "partial": 46,
"refused": 0,
- "not-implemented": 40,
+ "not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -28609,9 +28609,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 by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -28678,9 +28678,9 @@
"inherited": true
},
"static": {
- "status": "partial",
- "detail": "Implemented by a dedicated static compiler/runtime path.",
- "verification": "test-backed"
+ "status": "not-implemented",
+ "detail": "No static compiler lowering is registered for this API yet.",
+ "verification": "registry-gap"
}
},
{
@@ -28747,9 +28747,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 by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50037,9 +50037,9 @@
"anchor": "class-netblocklist",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50055,9 +50055,9 @@
"anchor": "blocklistaddaddressaddress-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50073,9 +50073,9 @@
"anchor": "blocklistaddaddressesaddresses-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50091,9 +50091,9 @@
"anchor": "blocklistaddcidrcidr",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50109,9 +50109,9 @@
"anchor": "blocklistaddcidrscidrs",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50127,9 +50127,9 @@
"anchor": "blocklistaddrangestart-end-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50145,9 +50145,9 @@
"anchor": "blocklistaddsubnetnet-prefix-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50163,9 +50163,9 @@
"anchor": "blocklistcheckaddress-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50181,9 +50181,9 @@
"anchor": "blocklistclear",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50204,9 +50204,9 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50222,9 +50222,9 @@
"anchor": "blocklistisblocklistvalue",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50240,9 +50240,9 @@
"anchor": "blocklistremoveaddressaddress-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50258,9 +50258,9 @@
"anchor": "blocklistremovecidrcidr",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50276,9 +50276,9 @@
"anchor": "blocklistremoverangestart-end-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50294,9 +50294,9 @@
"anchor": "blocklistremovesubnetnet-prefix-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50317,9 +50317,9 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50389,9 +50389,9 @@
"anchor": "class-netsocketaddress",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -50407,9 +50407,9 @@
"anchor": "socketaddressparseinput",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "supported",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -52503,9 +52503,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 call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -57593,9 +57593,9 @@
"anchor": "",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -65550,9 +65550,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -65573,9 +65573,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -65596,9 +65596,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -65619,9 +65619,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -65642,9 +65642,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -65665,9 +65665,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -66401,9 +66401,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -66424,9 +66424,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -67597,9 +67597,9 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -67620,9 +67620,9 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented by a dedicated static compiler/runtime path.",
+ "verification": "test-backed"
}
},
{
@@ -71599,9 +71599,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"
}
},
{
@@ -71714,9 +71714,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"
}
},
{
@@ -71806,9 +71806,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"
}
},
{
@@ -73324,9 +73324,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"
}
},
{
@@ -73347,9 +73347,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"
}
},
{
@@ -73508,9 +73508,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"
}
},
{
@@ -74060,9 +74060,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74083,9 +74083,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74106,9 +74106,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74129,9 +74129,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74152,9 +74152,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74175,9 +74175,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74221,9 +74221,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74244,9 +74244,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74267,9 +74267,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74451,9 +74451,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74497,9 +74497,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74566,9 +74566,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74681,9 +74681,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74704,9 +74704,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74727,9 +74727,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -74750,9 +74750,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Checks existing native runtime brands without inspecting public properties, with stored function values and tested predicate narrowing; unrepresented object forms retain compiler boundaries.",
+ "verification": "test-backed"
}
},
{
@@ -82920,9 +82920,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"
}
},
{
@@ -82943,9 +82943,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"
}
},
{
@@ -82966,9 +82966,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"
}
},
{
@@ -82989,9 +82989,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"
}
},
{
@@ -83012,9 +83012,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"
}
},
{
@@ -83035,9 +83035,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -83564,9 +83564,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -83633,9 +83633,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"
}
},
{
@@ -83656,9 +83656,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"
}
},
{
@@ -83679,9 +83679,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"
}
},
{
@@ -83702,9 +83702,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"
}
},
{
@@ -83748,9 +83748,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"
}
},
{
@@ -83886,9 +83886,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"
}
},
{
@@ -83909,9 +83909,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"
}
},
{
@@ -83932,9 +83932,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -83955,9 +83955,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -84047,9 +84047,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -84070,9 +84070,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
@@ -84139,9 +84139,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"
}
},
{
@@ -84208,9 +84208,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index 72c10cab0..12b12d63b 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -3,12 +3,12 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
- "taskCount": 3472,
- "tierItemCount": 3472,
+ "taskCount": 3456,
+ "tierItemCount": 3456,
"tiers": {
"static": {
- "audit-partial": 1086,
- "verify-gap": 2259,
+ "audit-partial": 1143,
+ "verify-gap": 2186,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -16,7 +16,7 @@
},
"priorities": {
"high": 2289,
- "normal": 710,
+ "normal": 694,
"low": 473
}
},
@@ -5885,7 +5885,8 @@
"source": "compiler-dedicated:console.assert",
"tests": [
"tests/corpus/5096-console-assert-dir.ts",
- "tests/corpus/5099-console-methods-dynamic.cjs"
+ "tests/corpus/5099-console-methods-dynamic.cjs",
+ "tests/corpus/5884-inspect-layout-options.ts"
]
}
}
@@ -6029,7 +6030,8 @@
"source": "compiler-dedicated:console.dir",
"tests": [
"tests/corpus/5096-console-assert-dir.ts",
- "tests/corpus/5099-console-methods-dynamic.cjs"
+ "tests/corpus/5099-console-methods-dynamic.cjs",
+ "tests/corpus/5884-inspect-layout-options.ts"
]
}
}
@@ -6057,7 +6059,8 @@
"source": "compiler-dedicated:console.dirxml",
"tests": [
"tests/corpus/5096-console-assert-dir.ts",
- "tests/corpus/5099-console-methods-dynamic.cjs"
+ "tests/corpus/5099-console-methods-dynamic.cjs",
+ "tests/corpus/5884-inspect-layout-options.ts"
]
}
}
@@ -25006,8 +25009,7 @@
"confidence": "high",
"source": "compiler-dedicated:events.getEventListeners",
"tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
+ "tests/corpus/5885-events-max-listener-helpers.ts"
]
}
}
@@ -25028,12 +25030,14 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:events",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-dedicated:events.getMaxListeners",
+ "tests": [
+ "tests/corpus/5885-events-max-listener-helpers.ts"
+ ]
}
}
},
@@ -25087,8 +25091,7 @@
"confidence": "high",
"source": "compiler-dedicated:events.listenerCount",
"tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
+ "tests/corpus/5885-events-max-listener-helpers.ts"
]
}
}
@@ -25109,15 +25112,12 @@
"priority": "high",
"tiers": {
"static": {
- "status": "partial",
- "action": "audit-partial",
- "verification": "test-backed",
- "confidence": "high",
- "source": "compiler-dedicated:events.on",
- "tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
- ]
+ "status": "not-implemented",
+ "action": "verify-gap",
+ "verification": "registry-gap",
+ "confidence": "low",
+ "source": "compiler-unmatched:events",
+ "tests": []
}
}
},
@@ -25143,8 +25143,7 @@
"confidence": "high",
"source": "compiler-dedicated:events.setMaxListeners",
"tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
+ "tests/corpus/5885-events-max-listener-helpers.ts"
]
}
}
@@ -25192,12 +25191,14 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:events",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-dedicated:events.defaultMaxListeners",
+ "tests": [
+ "tests/corpus/5885-events-max-listener-helpers.ts"
+ ]
}
}
},
@@ -34347,7 +34348,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -34377,7 +34379,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -34407,7 +34410,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -34487,7 +34491,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -34517,7 +34522,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -34547,7 +34553,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -49222,322 +49229,15 @@
"priority": "normal",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:aa4d5d08e9a5",
- "chapter": "net",
- "label": "blockList.addAddress(address[, type])",
- "signature": "`blockList.addAddress(address[, type])`",
- "apiSymbol": "blockList.addAddress",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistaddaddressaddress-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:bed278373fec",
- "chapter": "net",
- "label": "blockList.addAddresses(addresses[, type])",
- "signature": "`blockList.addAddresses(addresses[, type])`",
- "apiSymbol": "blockList.addAddresses",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistaddaddressesaddresses-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:3eab1fde8dda",
- "chapter": "net",
- "label": "blockList.addCIDR(cidr)",
- "signature": "`blockList.addCIDR(cidr)`",
- "apiSymbol": "blockList.addCIDR",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistaddcidrcidr",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:85bb2c28b8b4",
- "chapter": "net",
- "label": "blockList.addCIDRs(cidrs)",
- "signature": "`blockList.addCIDRs(cidrs)`",
- "apiSymbol": "blockList.addCIDRs",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistaddcidrscidrs",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:035828cd10e1",
- "chapter": "net",
- "label": "blockList.addRange(start, end[, type])",
- "signature": "`blockList.addRange(start, end[, type])`",
- "apiSymbol": "blockList.addRange",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistaddrangestart-end-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:a2c2a1e796ad",
- "chapter": "net",
- "label": "blockList.addSubnet(net, prefix[, type])",
- "signature": "`blockList.addSubnet(net, prefix[, type])`",
- "apiSymbol": "blockList.addSubnet",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistaddsubnetnet-prefix-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:278a3ce510c8",
- "chapter": "net",
- "label": "blockList.check(address[, type])",
- "signature": "`blockList.check(address[, type])`",
- "apiSymbol": "blockList.check",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistcheckaddress-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:e0681ee0179f",
- "chapter": "net",
- "label": "blockList.clear()",
- "signature": "`blockList.clear()`",
- "apiSymbol": "blockList.clear",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistclear",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:07e5de315d95",
- "chapter": "net",
- "label": "blockList.fromJSON(value)",
- "signature": "`blockList.fromJSON(value)`",
- "apiSymbol": "blockList.fromJSON",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistfromjsonvalue",
- "nodeStability": {
- "index": "1.2",
- "level": 1,
- "text": "Release candidate",
- "inherited": false
- },
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:4ff6a4c12f52",
- "chapter": "net",
- "label": "BlockList.isBlockList(value)",
- "signature": "`BlockList.isBlockList(value)`",
- "apiSymbol": "BlockList.isBlockList",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistisblocklistvalue",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:6ccaedabc10a",
- "chapter": "net",
- "label": "blockList.removeAddress(address[, type])",
- "signature": "`blockList.removeAddress(address[, type])`",
- "apiSymbol": "blockList.removeAddress",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistremoveaddressaddress-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:03c6467a8162",
- "chapter": "net",
- "label": "blockList.removeCIDR(cidr)",
- "signature": "`blockList.removeCIDR(cidr)`",
- "apiSymbol": "blockList.removeCIDR",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistremovecidrcidr",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:8ba4f8ec27e3",
- "chapter": "net",
- "label": "blockList.removeRange(start, end[, type])",
- "signature": "`blockList.removeRange(start, end[, type])`",
- "apiSymbol": "blockList.removeRange",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistremoverangestart-end-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:6cc98f6368e6",
- "chapter": "net",
- "label": "blockList.removeSubnet(net, prefix[, type])",
- "signature": "`blockList.removeSubnet(net, prefix[, type])`",
- "apiSymbol": "blockList.removeSubnet",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklistremovesubnetnet-prefix-type",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:fb2019fd5533",
- "chapter": "net",
- "label": "blockList.toJSON()",
- "signature": "`blockList.toJSON()`",
- "apiSymbol": "blockList.toJSON",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#blocklisttojson",
- "nodeStability": {
- "index": "1.2",
- "level": 1,
- "text": "Release candidate",
- "inherited": false
- },
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-dedicated:net.BlockList",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
}
}
},
@@ -49612,32 +49312,15 @@
"priority": "normal",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
- }
- }
- },
- {
- "id": "net:d8b7ddef1a9e",
- "chapter": "net",
- "label": "SocketAddress.parse(input)",
- "signature": "`SocketAddress.parse(input)`",
- "apiSymbol": "SocketAddress.parse",
- "docsUrl": "https://nodejs.org/docs/v24.22.0/api/net.html#socketaddressparseinput",
- "nodeStability": null,
- "priority": "normal",
- "tiers": {
- "static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:net",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-dedicated:net.SocketAddress",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
}
}
},
@@ -52059,12 +51742,14 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:os",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.os.setPriority",
+ "tests": [
+ "tests/corpus/5883-os-set-priority.ts"
+ ]
}
}
},
@@ -57822,12 +57507,15 @@
"priority": "normal",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:process",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.process.execArgv",
+ "tests": [
+ "tests/corpus/990-process-basics.ts",
+ "tests/corpus/2314-process-introspection.ts"
+ ]
}
}
},
@@ -57871,6 +57559,7 @@
"confidence": "high",
"source": "surface-manifest:node-builtin.process.exitCode",
"tests": [
+ "tests/corpus/5809-process-exit-and-rejections.cjs",
"tests/corpus/2941-process-exit-code.ts",
"tests/corpus/2942-process-exit-code-override.ts",
"tests/corpus/2943-process-exit-code-invalid.ts",
@@ -65937,7 +65626,8 @@
"source": "compiler-dedicated:StringDecoder",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
}
}
@@ -65965,7 +65655,8 @@
"source": "compiler-dedicated:stringDecoder.end",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
}
}
@@ -65993,7 +65684,8 @@
"source": "compiler-dedicated:stringDecoder.write",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
}
}
@@ -66990,12 +66682,18 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.timers.enable",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -67015,12 +66713,18 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.timers.reset",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -67040,12 +66744,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.timers",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -67065,12 +66772,18 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.timers.tick",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -67090,12 +66803,18 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.timers.runAll",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -67115,12 +66834,18 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.timers.setTime",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -67945,12 +67670,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.context.assert.fileSnapshot",
+ "tests": [
+ "tests/fixtures/node-test/cases/snapshot.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -67970,12 +67698,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:test",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:test.context.assert.snapshot",
+ "tests": [
+ "tests/fixtures/node-test/cases/snapshot.ts",
+ "tests/harness/node-test.test.ts"
+ ]
}
}
},
@@ -69077,7 +68808,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69107,7 +68839,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69137,7 +68870,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69167,7 +68901,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69197,7 +68932,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69227,7 +68963,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69257,7 +68994,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69287,7 +69025,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69317,7 +69056,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69338,12 +69078,15 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:timers",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-dedicated:timersPromises.scheduler.wait",
+ "tests": [
+ "tests/corpus/5839-timers-promises-scheduler.ts",
+ "tests/corpus/5889-timers-scheduler-signal.ts"
+ ]
}
}
},
@@ -69363,12 +69106,15 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:timers",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-dedicated:timersPromises.scheduler.yield",
+ "tests": [
+ "tests/corpus/5839-timers-promises-scheduler.ts",
+ "tests/corpus/5889-timers-scheduler-signal.ts"
+ ]
}
}
},
@@ -69422,7 +69168,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69452,7 +69199,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69482,7 +69230,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69589,7 +69338,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69619,7 +69369,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69649,7 +69400,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -69679,7 +69431,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
}
}
@@ -73753,12 +73506,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:url",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:url.urlSearchParams.entries",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
}
}
},
@@ -73878,12 +73634,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:url",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:url.urlSearchParams.keys",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
}
}
},
@@ -73978,12 +73737,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:url",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:url.urlSearchParams.values",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
}
}
},
@@ -75797,12 +75559,16 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.format",
+ "tests": [
+ "tests/corpus/1636-util-format.ts",
+ "tests/corpus/2440-console-inspect-args.ts",
+ "tests/corpus/5835-util-format-integer-specifiers.ts"
+ ]
}
}
},
@@ -75822,12 +75588,16 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.formatWithOptions",
+ "tests": [
+ "tests/corpus/1636-util-format.ts",
+ "tests/corpus/2440-console-inspect-args.ts",
+ "tests/corpus/5835-util-format-integer-specifiers.ts"
+ ]
}
}
},
@@ -76016,12 +75786,18 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.inspect",
+ "tests": [
+ "tests/corpus/1630-inspect-scalars.ts",
+ "tests/corpus/1632-inspect-records.ts",
+ "tests/corpus/1637-inspect-dyn.ts",
+ "tests/corpus/2485-inspect-circular-refs.ts",
+ "tests/corpus/5884-inspect-layout-options.ts"
+ ]
}
}
},
@@ -76700,12 +76476,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isBigInt64Array",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76725,12 +76508,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isBigIntObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76750,12 +76540,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isBigUint64Array",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76775,12 +76572,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isBooleanObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76800,12 +76604,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isBoxedPrimitive",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76825,12 +76636,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isCryptoKey",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76880,12 +76698,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isDate",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76905,12 +76730,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isExternal",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -76930,12 +76762,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isFloat16Array",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -77165,12 +77004,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isKeyObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -77220,12 +77066,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isMapIterator",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -77300,12 +77153,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isNumberObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -77445,12 +77305,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isSetIterator",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -77470,12 +77337,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isSharedArrayBuffer",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -77495,12 +77369,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isStringObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -77520,12 +77401,19 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:util",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:util.util.types.isSymbolObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
}
}
},
@@ -83954,12 +83842,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.BroadcastChannel extends EventTarget",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
}
}
},
@@ -83979,12 +83871,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.broadcastChannel.close",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
}
}
},
@@ -84004,12 +83900,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.broadcastChannel.postMessage",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
}
}
},
@@ -84029,12 +83929,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.broadcastChannel.ref",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
}
}
},
@@ -84054,12 +83958,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.broadcastChannel.unref",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
}
}
},
@@ -84079,12 +83987,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.onmessage",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
}
}
},
@@ -84137,7 +84049,8 @@
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
- "tests/corpus/5797-worker-message-port-transfer.ts"
+ "tests/corpus/5797-worker-message-port-transfer.ts",
+ "tests/corpus/5805-worker-receive-message-parent-port.ts"
]
}
}
@@ -84702,12 +84615,14 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:worker_threads",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:worker_threads.worker",
+ "tests": [
+ "tests/corpus/5991-worker-async-dispose.mjs"
+ ]
}
}
},
@@ -84777,12 +84692,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.resourceLimits",
+ "tests": [
+ "tests/corpus/5806-worker-resource-limits.ts"
+ ]
}
}
},
@@ -84802,12 +84719,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.stderr",
+ "tests": [
+ "tests/corpus/5874-worker-stdio-streams.ts",
+ "tests/corpus/5875-worker-stdout-pipe.cjs",
+ "tests/corpus/5879-worker-stdin.ts"
+ ]
}
}
},
@@ -84827,12 +84748,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.stdin",
+ "tests": [
+ "tests/corpus/5874-worker-stdio-streams.ts",
+ "tests/corpus/5875-worker-stdout-pipe.cjs",
+ "tests/corpus/5879-worker-stdin.ts"
+ ]
}
}
},
@@ -84852,12 +84777,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.stdout",
+ "tests": [
+ "tests/corpus/5874-worker-stdio-streams.ts",
+ "tests/corpus/5875-worker-stdout-pipe.cjs",
+ "tests/corpus/5879-worker-stdin.ts"
+ ]
}
}
},
@@ -84906,12 +84835,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.threadName",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
}
}
},
@@ -84939,7 +84871,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
}
}
@@ -84968,7 +84902,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
}
}
@@ -84997,7 +84933,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
}
}
@@ -85051,7 +84989,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
}
}
@@ -85072,12 +85012,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.worker_threads.getEnvironmentData",
+ "tests": [
+ "tests/corpus/5803-worker-environment-data.ts",
+ "tests/corpus/5804-worker-environment-data.cjs"
+ ]
}
}
},
@@ -85097,12 +85040,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.worker_threads.markAsUntransferable",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
}
}
},
@@ -85122,12 +85068,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.worker_threads.isMarkedAsUntransferable",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
}
}
},
@@ -85147,12 +85096,14 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:worker_threads",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:worker_threads.worker_threads.markAsUncloneable",
+ "tests": [
+ "tests/corpus/5990-worker-uncloneable.mjs"
+ ]
}
}
},
@@ -85230,7 +85181,8 @@
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
- "tests/corpus/5797-worker-message-port-transfer.ts"
+ "tests/corpus/5797-worker-message-port-transfer.ts",
+ "tests/corpus/5805-worker-receive-message-parent-port.ts"
]
}
}
@@ -85251,12 +85203,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.worker_threads.setEnvironmentData",
+ "tests": [
+ "tests/corpus/5803-worker-environment-data.ts",
+ "tests/corpus/5804-worker-environment-data.cjs"
+ ]
}
}
},
@@ -85276,12 +85231,14 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:worker_threads",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:worker_threads.isInternalThread",
+ "tests": [
+ "tests/corpus/5990-worker-uncloneable.mjs"
+ ]
}
}
},
@@ -85359,12 +85316,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.resourceLimits",
+ "tests": [
+ "tests/corpus/5806-worker-resource-limits.ts"
+ ]
}
}
},
@@ -85438,12 +85397,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.threadName",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index e50ae977d..2b97f675e 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -311,9 +311,9 @@
"apiEntries": 78,
"static": {
"supported": 18,
- "partial": 34,
+ "partial": 35,
"refused": 0,
- "not-implemented": 26,
+ "not-implemented": 25,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -520,10 +520,10 @@
"entries": 122,
"apiEntries": 119,
"static": {
- "supported": 0,
- "partial": 37,
+ "supported": 16,
+ "partial": 39,
"refused": 0,
- "not-implemented": 82,
+ "not-implemented": 64,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -536,9 +536,9 @@
"apiEntries": 24,
"static": {
"supported": 0,
- "partial": 23,
+ "partial": 24,
"refused": 0,
- "not-implemented": 1,
+ "not-implemented": 0,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -596,9 +596,9 @@
"apiEntries": 120,
"static": {
"supported": 0,
- "partial": 50,
+ "partial": 51,
"refused": 0,
- "not-implemented": 70,
+ "not-implemented": 69,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -746,9 +746,9 @@
"apiEntries": 117,
"static": {
"supported": 0,
- "partial": 45,
+ "partial": 53,
"refused": 0,
- "not-implemented": 72,
+ "not-implemented": 64,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -761,9 +761,9 @@
"apiEntries": 25,
"static": {
"supported": 0,
- "partial": 20,
+ "partial": 22,
"refused": 0,
- "not-implemented": 5,
+ "not-implemented": 3,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -836,9 +836,9 @@
"apiEntries": 62,
"static": {
"supported": 0,
- "partial": 39,
+ "partial": 42,
"refused": 0,
- "not-implemented": 23,
+ "not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -851,9 +851,9 @@
"apiEntries": 109,
"static": {
"supported": 0,
- "partial": 53,
+ "partial": 72,
"refused": 0,
- "not-implemented": 56,
+ "not-implemented": 37,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -941,9 +941,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
- "partial": 26,
+ "partial": 46,
"refused": 0,
- "not-implemented": 40,
+ "not-implemented": 20,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -19141,6 +19141,7 @@
"tests/corpus/5097-console-table.ts",
"tests/corpus/5098-console-time-warnings.ts",
"tests/corpus/5099-console-methods-dynamic.cjs",
+ "tests/corpus/5884-inspect-layout-options.ts",
"tests/corpus/console-native-value.js",
"tests/harness/npm-static.test.ts"
]
@@ -19297,7 +19298,8 @@
"evidence": "compiler-dedicated:console.assert",
"tests": [
"tests/corpus/5096-console-assert-dir.ts",
- "tests/corpus/5099-console-methods-dynamic.cjs"
+ "tests/corpus/5099-console-methods-dynamic.cjs",
+ "tests/corpus/5884-inspect-layout-options.ts"
]
},
"anchorSource": "exact"
@@ -19436,7 +19438,8 @@
"evidence": "compiler-dedicated:console.dir",
"tests": [
"tests/corpus/5096-console-assert-dir.ts",
- "tests/corpus/5099-console-methods-dynamic.cjs"
+ "tests/corpus/5099-console-methods-dynamic.cjs",
+ "tests/corpus/5884-inspect-layout-options.ts"
]
},
"anchorSource": "exact"
@@ -19463,7 +19466,8 @@
"evidence": "compiler-dedicated:console.dirxml",
"tests": [
"tests/corpus/5096-console-assert-dir.ts",
- "tests/corpus/5099-console-methods-dynamic.cjs"
+ "tests/corpus/5099-console-methods-dynamic.cjs",
+ "tests/corpus/5884-inspect-layout-options.ts"
]
},
"anchorSource": "exact"
@@ -41814,7 +41818,8 @@
"tests/corpus/5244-event-target-subclass.js",
"tests/corpus/5245-event-target-subclass-signals.ts",
"tests/corpus/5247-event-target-subclass-lifetimes.ts",
- "tests/corpus/5248-event-target-subclass-warnings.ts"
+ "tests/corpus/5248-event-target-subclass-warnings.ts",
+ "tests/corpus/5885-events-max-listener-helpers.ts"
]
},
"anchorSource": "chapter"
@@ -43734,8 +43739,7 @@
"status": "partial",
"evidence": "compiler-dedicated:events.getEventListeners",
"tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
+ "tests/corpus/5885-events-max-listener-helpers.ts"
]
},
"anchorSource": "exact"
@@ -43758,8 +43762,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:events"
+ "status": "partial",
+ "evidence": "compiler-dedicated:events.getMaxListeners",
+ "tests": [
+ "tests/corpus/5885-events-max-listener-helpers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -43834,8 +43841,7 @@
"status": "partial",
"evidence": "compiler-dedicated:events.listenerCount",
"tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
+ "tests/corpus/5885-events-max-listener-helpers.ts"
]
},
"anchorSource": "exact"
@@ -43858,12 +43864,8 @@
"inherited": true
},
"static": {
- "status": "partial",
- "evidence": "compiler-dedicated:events.on",
- "tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
- ]
+ "status": "not-implemented",
+ "evidence": "compiler-unmatched:events"
},
"anchorSource": "exact"
},
@@ -43888,8 +43890,7 @@
"status": "partial",
"evidence": "compiler-dedicated:events.setMaxListeners",
"tests": [
- "tests/corpus/1677-emitter-listeners.ts",
- "tests/corpus/2620-ee-override-once-order.ts"
+ "tests/corpus/5885-events-max-listener-helpers.ts"
]
},
"anchorSource": "exact"
@@ -43938,8 +43939,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:events"
+ "status": "partial",
+ "evidence": "compiler-dedicated:events.defaultMaxListeners",
+ "tests": [
+ "tests/corpus/5885-events-max-listener-helpers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -52160,6 +52164,7 @@
"tests/corpus/5287-blob-inspect.ts",
"tests/corpus/5288-blob-validation.js",
"tests/corpus/5290-blob-dynamic.js",
+ "tests/corpus/5830-timers-promises-signal.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/harness/fetch-conformance.test.ts"
]
@@ -53663,7 +53668,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -53692,7 +53698,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -53721,7 +53728,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -53796,7 +53804,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -53825,7 +53834,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -53854,7 +53864,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -71655,7 +71666,9 @@
"tests/corpus/2596-net-arg-ladders.cjs",
"tests/corpus/2597-dgram-send-ladders.cjs",
"tests/corpus/2672-http-request-response-callback.ts",
- "tests/corpus/2689-http-server-timeout-properties.ts"
+ "tests/corpus/2689-http-server-timeout-properties.ts",
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
]
},
"anchorSource": "chapter"
@@ -71709,8 +71722,12 @@
"anchor": "class-netblocklist",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "partial",
+ "evidence": "compiler-dedicated:net.BlockList",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71727,8 +71744,12 @@
"anchor": "blocklistaddaddressaddress-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.addAddress",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71745,8 +71766,12 @@
"anchor": "blocklistaddaddressesaddresses-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.addAddresses",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71763,8 +71788,12 @@
"anchor": "blocklistaddcidrcidr",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.addCIDR",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71781,8 +71810,12 @@
"anchor": "blocklistaddcidrscidrs",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.addCIDRs",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71799,8 +71832,12 @@
"anchor": "blocklistaddrangestart-end-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.addRange",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71817,8 +71854,12 @@
"anchor": "blocklistaddsubnetnet-prefix-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.addSubnet",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71835,8 +71876,12 @@
"anchor": "blocklistcheckaddress-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.check",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71853,8 +71898,12 @@
"anchor": "blocklistclear",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.clear",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71876,8 +71925,12 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.fromJSON",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71894,8 +71947,12 @@
"anchor": "blocklistisblocklistvalue",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:BlockList.isBlockList",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71912,8 +71969,12 @@
"anchor": "blocklistremoveaddressaddress-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.removeAddress",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71930,8 +71991,12 @@
"anchor": "blocklistremovecidrcidr",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.removeCIDR",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71948,8 +72013,12 @@
"anchor": "blocklistremoverangestart-end-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.removeRange",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71966,8 +72035,12 @@
"anchor": "blocklistremovesubnetnet-prefix-type",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.removeSubnet",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -71989,8 +72062,12 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:blockList.toJSON",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -72061,8 +72138,12 @@
"anchor": "class-netsocketaddress",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "partial",
+ "evidence": "compiler-dedicated:net.SocketAddress",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -72079,8 +72160,12 @@
"anchor": "socketaddressparseinput",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:net"
+ "status": "supported",
+ "evidence": "compiler-dedicated:SocketAddress.parse",
+ "tests": [
+ "tests/corpus/5659-net-blocklist-socketaddress.ts",
+ "tests/fixtures/node-types/net-blocklist.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -74016,7 +74101,8 @@
"tests/corpus/2917-builtin-callable-widths.ts",
"tests/corpus/4598-os-available-parallelism.ts",
"tests/corpus/5140-os-host-facts.ts",
- "tests/corpus/5141-os-constants.ts"
+ "tests/corpus/5141-os-constants.ts",
+ "tests/corpus/5883-os-set-priority.ts"
]
},
"anchorSource": "chapter"
@@ -74565,8 +74651,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:os"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.os.setPriority",
+ "tests": [
+ "tests/corpus/5883-os-set-priority.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -79208,6 +79297,7 @@
"tests/corpus/5194-process-emit-warning-tick.cjs",
"tests/corpus/5210-process-memory-usage.ts",
"tests/corpus/5211-process-identity.ts",
+ "tests/corpus/5809-process-exit-and-rejections.cjs",
"tests/corpus/990-process-basics.ts",
"tests/corpus/node24-env-files-errors.ts",
"tests/corpus/node24-env-files.ts",
@@ -80859,8 +80949,12 @@
"anchor": "",
"nodeStability": null,
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:process"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.process.execArgv",
+ "tests": [
+ "tests/corpus/990-process-basics.ts",
+ "tests/corpus/2314-process-introspection.ts"
+ ]
},
"anchorSource": "chapter"
},
@@ -80902,6 +80996,7 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.process.exitCode",
"tests": [
+ "tests/corpus/5809-process-exit-and-rejections.cjs",
"tests/corpus/2941-process-exit-code.ts",
"tests/corpus/2942-process-exit-code-override.ts",
"tests/corpus/2943-process-exit-code-invalid.ts",
@@ -90732,6 +90827,7 @@
"evidence": "derived:descendants",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts",
"tests/corpus/node24-string-decoder.ts"
]
},
@@ -90759,7 +90855,8 @@
"evidence": "compiler-dedicated:StringDecoder",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
},
"anchorSource": "exact"
@@ -90786,7 +90883,8 @@
"evidence": "compiler-dedicated:stringDecoder.end",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
},
"anchorSource": "exact"
@@ -90813,7 +90911,8 @@
"evidence": "compiler-dedicated:stringDecoder.write",
"tests": [
"tests/corpus/1662-string-decoder-encodings.ts",
- "tests/corpus/node24-string-decoder.ts"
+ "tests/corpus/node24-string-decoder.ts",
+ "tests/corpus/5882-string-decoder-runtime-encoding.ts"
]
},
"anchorSource": "exact"
@@ -90847,11 +90946,16 @@
"tests/corpus/5515-assert-deep-strict-typed-messages.ts",
"tests/corpus/5810-mock-method-records.ts",
"tests/corpus/5811-mock-tracker-restore.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
"tests/fixtures/node-test/cases/ctx-hooks.ts",
"tests/fixtures/node-test/cases/ctx-plan.ts",
"tests/fixtures/node-test/cases/mock-fn.ts",
"tests/fixtures/node-test/cases/mock-method.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/fixtures/node-test/cases/mock-timers.ts",
"tests/fixtures/node-test/cases/only-nested.ts",
+ "tests/fixtures/node-test/cases/snapshot.ts",
"tests/fixtures/node-test/cases/suite-variants.ts",
"tests/fixtures/node-test/cases/todo-suites.ts",
"tests/harness/node-test.test.ts"
@@ -92481,8 +92585,15 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.timers.enable",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -92504,8 +92615,15 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.timers.reset",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -92527,8 +92645,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.timers",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -92550,8 +92672,15 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.timers.tick",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -92619,8 +92748,15 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.timers.runAll",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -92642,8 +92778,15 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.timers.setTime",
+ "tests": [
+ "tests/fixtures/node-test/cases/mock-timers.ts",
+ "tests/fixtures/node-test/cases/mock-timers-date.ts",
+ "tests/corpus/5813-mock-timers-module.ts",
+ "tests/corpus/5814-mock-timers-date.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -93464,8 +93607,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.context.assert.fileSnapshot",
+ "tests": [
+ "tests/fixtures/node-test/cases/snapshot.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -93487,8 +93634,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:test"
+ "status": "partial",
+ "evidence": "compiler-feature:test.context.assert.snapshot",
+ "tests": [
+ "tests/fixtures/node-test/cases/snapshot.ts",
+ "tests/harness/node-test.test.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -94535,7 +94686,10 @@
"tests/corpus/2672-http-request-response-callback.ts",
"tests/corpus/2689-http-server-timeout-properties.ts",
"tests/corpus/2844-node-disposable-resources.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts",
+ "tests/corpus/5839-timers-promises-scheduler.ts",
+ "tests/corpus/5889-timers-scheduler-signal.ts"
]
},
"anchorSource": "chapter"
@@ -94587,7 +94741,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94616,7 +94771,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94645,7 +94801,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94697,7 +94854,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94726,7 +94884,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94755,7 +94914,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94807,7 +94967,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94836,7 +94997,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94865,7 +95027,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94888,8 +95051,12 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:timers"
+ "status": "partial",
+ "evidence": "compiler-dedicated:timersPromises.scheduler.wait",
+ "tests": [
+ "tests/corpus/5839-timers-promises-scheduler.ts",
+ "tests/corpus/5889-timers-scheduler-signal.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -94911,8 +95078,12 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:timers"
+ "status": "partial",
+ "evidence": "compiler-dedicated:timersPromises.scheduler.yield",
+ "tests": [
+ "tests/corpus/5839-timers-promises-scheduler.ts",
+ "tests/corpus/5889-timers-scheduler-signal.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -94963,7 +95134,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -94992,7 +95164,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -95021,7 +95194,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -95122,7 +95296,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -95151,7 +95326,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -95180,7 +95356,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -95209,7 +95386,8 @@
"tests/corpus/1804-timers-module.ts",
"tests/corpus/1805-timer-callback-args.ts",
"tests/corpus/2093-timers-promises.ts",
- "tests/corpus/2852-timers-promises-interval.ts"
+ "tests/corpus/2852-timers-promises-interval.ts",
+ "tests/corpus/5830-timers-promises-signal.ts"
]
},
"anchorSource": "exact"
@@ -98896,6 +99074,8 @@
"tests/corpus/5227-url-legacy-dependency/main.ts",
"tests/corpus/5228-url-domain-conversion.ts",
"tests/corpus/5229-url-legacy-errors.cjs",
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs",
"tests/corpus/node24-file-url-buffer.ts",
"tests/corpus/node24-file-url-errors.js",
"tests/corpus/node24-file-url-options.ts",
@@ -99680,8 +99860,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:url"
+ "status": "partial",
+ "evidence": "compiler-feature:url.urlSearchParams.entries",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -99795,8 +99979,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:url"
+ "status": "partial",
+ "evidence": "compiler-feature:url.urlSearchParams.keys",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -99887,8 +100075,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:url"
+ "status": "partial",
+ "evidence": "compiler-feature:url.urlSearchParams.values",
+ "tests": [
+ "tests/corpus/5833-url-search-params-iterators.ts",
+ "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -100766,6 +100958,12 @@
"packages/compiler/test/deep-equality.test.ts",
"tests/corpus/1473-promisify-execfile.ts",
"tests/corpus/1573-promisify-execfile-env-spread.ts",
+ "tests/corpus/1630-inspect-scalars.ts",
+ "tests/corpus/1632-inspect-records.ts",
+ "tests/corpus/1636-util-format.ts",
+ "tests/corpus/1637-inspect-dyn.ts",
+ "tests/corpus/2440-console-inspect-args.ts",
+ "tests/corpus/2485-inspect-circular-refs.ts",
"tests/corpus/2678-util-parseargs.ts",
"tests/corpus/2912-promisify-fs-readfile.ts",
"tests/corpus/3133-text-decoder-streaming.ts",
@@ -100788,6 +100986,10 @@
"tests/corpus/5628-promisify-timers.ts",
"tests/corpus/5629-promisify-stream.ts",
"tests/corpus/5630-promisify-zlib.ts",
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "tests/corpus/5835-util-format-integer-specifiers.ts",
+ "tests/corpus/5884-inspect-layout-options.ts",
"tests/corpus/node24-deep-equality-properties.js",
"tests/corpus/node24-deep-equality.cjs",
"tests/corpus/node24-deep-equality.ts",
@@ -101884,8 +102086,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.format",
+ "tests": [
+ "tests/corpus/1636-util-format.ts",
+ "tests/corpus/2440-console-inspect-args.ts",
+ "tests/corpus/5835-util-format-integer-specifiers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -101907,8 +102114,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.formatWithOptions",
+ "tests": [
+ "tests/corpus/1636-util-format.ts",
+ "tests/corpus/2440-console-inspect-args.ts",
+ "tests/corpus/5835-util-format-integer-specifiers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -102091,8 +102303,15 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.inspect",
+ "tests": [
+ "tests/corpus/1630-inspect-scalars.ts",
+ "tests/corpus/1632-inspect-records.ts",
+ "tests/corpus/1637-inspect-dyn.ts",
+ "tests/corpus/2485-inspect-circular-refs.ts",
+ "tests/corpus/5884-inspect-layout-options.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -102855,8 +103074,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isBigInt64Array",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -102878,8 +103105,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isBigIntObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -102901,8 +103136,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isBigUint64Array",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -102924,8 +103167,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isBooleanObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -102947,8 +103198,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isBoxedPrimitive",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -102970,8 +103229,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isCryptoKey",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103022,8 +103289,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isDate",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103045,8 +103320,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isExternal",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103068,8 +103351,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isFloat16Array",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103294,8 +103585,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isKeyObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103346,8 +103645,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isMapIterator",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103421,8 +103728,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isNumberObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103560,8 +103875,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isSetIterator",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103583,8 +103906,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isSharedArrayBuffer",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103606,8 +103937,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isStringObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -103629,8 +103968,16 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:util"
+ "status": "partial",
+ "evidence": "compiler-feature:util.util.types.isSymbolObject",
+ "tests": [
+ "tests/corpus/5831-util-types-brands.ts",
+ "tests/corpus/5832-util-types-brands-dynamic.js",
+ "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"
+ ]
},
"anchorSource": "exact"
},
@@ -112740,6 +113087,22 @@
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
"tests/corpus/5797-worker-message-port-transfer.ts",
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts",
+ "tests/corpus/5803-worker-environment-data.ts",
+ "tests/corpus/5804-worker-environment-data.cjs",
+ "tests/corpus/5805-worker-receive-message-parent-port.ts",
+ "tests/corpus/5806-worker-resource-limits.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts",
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs",
+ "tests/corpus/5874-worker-stdio-streams.ts",
+ "tests/corpus/5875-worker-stdout-pipe.cjs",
+ "tests/corpus/5879-worker-stdin.ts",
+ "tests/corpus/5990-worker-uncloneable.mjs",
+ "tests/corpus/5991-worker-async-dispose.mjs",
"tests/corpus/native-worker-clone-errors.ts",
"tests/corpus/native-worker-commonjs.cjs",
"tests/corpus/native-worker-delayed-messages.ts",
@@ -112847,8 +113210,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.BroadcastChannel extends EventTarget",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -112870,8 +113238,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.broadcastChannel.close",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -112893,8 +113266,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.broadcastChannel.postMessage",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -112916,8 +113294,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.broadcastChannel.ref",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -112939,8 +113322,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.broadcastChannel.unref",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -112962,8 +113350,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.onmessage",
+ "tests": [
+ "tests/corpus/5800-broadcast-channel.ts",
+ "tests/corpus/5801-broadcast-channel.cjs",
+ "tests/corpus/5802-broadcast-channel-workers.ts"
+ ]
},
"anchorSource": "ancestor"
},
@@ -113013,7 +113406,8 @@
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
- "tests/corpus/5797-worker-message-port-transfer.ts"
+ "tests/corpus/5797-worker-message-port-transfer.ts",
+ "tests/corpus/5805-worker-receive-message-parent-port.ts"
]
},
"anchorSource": "exact"
@@ -113600,8 +113994,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker",
+ "tests": [
+ "tests/corpus/5991-worker-async-dispose.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -113669,8 +114066,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.resourceLimits",
+ "tests": [
+ "tests/corpus/5806-worker-resource-limits.ts"
+ ]
},
"anchorSource": "ancestor"
},
@@ -113692,8 +114092,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.stderr",
+ "tests": [
+ "tests/corpus/5874-worker-stdio-streams.ts",
+ "tests/corpus/5875-worker-stdout-pipe.cjs",
+ "tests/corpus/5879-worker-stdin.ts"
+ ]
},
"anchorSource": "ancestor"
},
@@ -113715,8 +114120,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.stdin",
+ "tests": [
+ "tests/corpus/5874-worker-stdio-streams.ts",
+ "tests/corpus/5875-worker-stdout-pipe.cjs",
+ "tests/corpus/5879-worker-stdin.ts"
+ ]
},
"anchorSource": "ancestor"
},
@@ -113738,8 +114148,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.stdout",
+ "tests": [
+ "tests/corpus/5874-worker-stdio-streams.ts",
+ "tests/corpus/5875-worker-stdout-pipe.cjs",
+ "tests/corpus/5879-worker-stdin.ts"
+ ]
},
"anchorSource": "ancestor"
},
@@ -113789,8 +114204,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.threadName",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
},
"anchorSource": "ancestor"
},
@@ -113817,7 +114236,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
},
"anchorSource": "exact"
@@ -113845,7 +114266,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
},
"anchorSource": "exact"
@@ -113873,7 +114296,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
},
"anchorSource": "exact"
@@ -113924,7 +114349,9 @@
"tests": [
"tests/corpus/native-worker-references.ts",
"tests/corpus/native-worker-error.ts",
- "tests/corpus/native-worker-exit-listeners.ts"
+ "tests/corpus/native-worker-exit-listeners.ts",
+ "tests/corpus/5807-worker-error-values.ts",
+ "tests/corpus/5808-worker-unhandled-rejections.ts"
]
},
"anchorSource": "exact"
@@ -113947,8 +114374,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker_threads.getEnvironmentData",
+ "tests": [
+ "tests/corpus/5803-worker-environment-data.ts",
+ "tests/corpus/5804-worker-environment-data.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -113970,8 +114401,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker_threads.markAsUntransferable",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -113993,8 +114428,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker_threads.isMarkedAsUntransferable",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -114016,8 +114455,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker_threads.markAsUncloneable",
+ "tests": [
+ "tests/corpus/5990-worker-uncloneable.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -114090,7 +114532,8 @@
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
- "tests/corpus/5797-worker-message-port-transfer.ts"
+ "tests/corpus/5797-worker-message-port-transfer.ts",
+ "tests/corpus/5805-worker-receive-message-parent-port.ts"
]
},
"anchorSource": "exact"
@@ -114113,8 +114556,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.worker_threads.setEnvironmentData",
+ "tests": [
+ "tests/corpus/5803-worker-environment-data.ts",
+ "tests/corpus/5804-worker-environment-data.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -114136,8 +114583,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.isInternalThread",
+ "tests": [
+ "tests/corpus/5990-worker-uncloneable.mjs"
+ ]
},
"anchorSource": "chapter"
},
@@ -114215,8 +114665,11 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.resourceLimits",
+ "tests": [
+ "tests/corpus/5806-worker-resource-limits.ts"
+ ]
},
"anchorSource": "chapter"
},
@@ -114289,8 +114742,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:worker_threads"
+ "status": "partial",
+ "evidence": "compiler-feature:worker_threads.threadName",
+ "tests": [
+ "tests/corpus/5870-worker-thread-names.ts",
+ "tests/corpus/5871-worker-untransferable.cjs"
+ ]
},
"anchorSource": "chapter"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index 51b8c2064..cdd025b72 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -104,6 +104,7 @@
"node-builtin.os.endianness": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.freemem": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.getPriority": ["tests/corpus/5140-os-host-facts.ts"],
+ "node-builtin.os.setPriority": ["tests/corpus/5883-os-set-priority.ts"],
"node-builtin.os.hostname": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.loadavg": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.machine": ["tests/corpus/5140-os-host-facts.ts"],
@@ -145,7 +146,7 @@
"node-builtin.process.versions": ["tests/corpus/process-versions-storage.ts"],
"node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"],
- "node-builtin.process.exitCode": ["tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
+ "node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
@@ -204,10 +205,20 @@
{"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: `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", "tests/corpus/5807-worker-error-values.ts", "tests/corpus/5808-worker-unhandled-rejections.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": "worker_threads", "symbols": ["MessageChannel", "worker_threads.receiveMessageOnPort"], "status": "partial", "evidence": ["tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts"]},
+ {"chapter": "worker_threads", "symbols": ["MessageChannel", "worker_threads.receiveMessageOnPort"], "status": "partial", "evidence": ["tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts", "tests/corpus/5805-worker-receive-message-parent-port.ts"]},
+ {"chapter": "worker_threads", "symbols": ["worker_threads.getEnvironmentData", "worker_threads.setEnvironmentData"], "status": "partial", "evidence": ["tests/corpus/5803-worker-environment-data.ts", "tests/corpus/5804-worker-environment-data.cjs"]},
+ {"chapter": "worker_threads", "symbols": ["stdout", "stderr", "stdin"], "status": "partial", "evidence": ["tests/corpus/5874-worker-stdio-streams.ts", "tests/corpus/5875-worker-stdout-pipe.cjs", "tests/corpus/5879-worker-stdin.ts"]},
+ {"chapter": "worker_threads", "symbols": ["threadName", "worker_threads.markAsUntransferable", "worker_threads.isMarkedAsUntransferable"], "status": "partial", "evidence": ["tests/corpus/5870-worker-thread-names.ts", "tests/corpus/5871-worker-untransferable.cjs"]},
+ {"chapter": "worker_threads", "symbols": ["worker_threads.markAsUncloneable", "isInternalThread"], "status": "partial", "evidence": ["tests/corpus/5990-worker-uncloneable.mjs"]},
+ {"chapter": "worker_threads", "signatures": ["`worker[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/5991-worker-async-dispose.mjs"]},
+ {"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]},
+ {"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
+ {"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.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", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "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", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.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"] },
{ "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" },
@@ -337,14 +348,18 @@
{ "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] },
{ "chapter": "url", "symbols": ["url.fileURLToPath", "url.fileURLToPathBuffer", "url.pathToFileURL"], "status": "partial", "evidence": ["tests/corpus/node24-file-url-buffer.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js", "tests/fixtures/node-types/file-url-conversion.ts"] },
{ "chapter": "url", "symbols": ["URL.canParse", "URL.parse", "url.toString", "url.toJSON"], "status": "partial", "evidence": ["tests/corpus/node24-url-factories.ts", "tests/corpus/node24-url-factory-errors.ts", "tests/fixtures/node-types/url-factories.ts", "tests/harness/project-config.test.ts"] },
+ { "chapter": "url", "symbols": ["urlSearchParams.entries", "urlSearchParams.keys", "urlSearchParams.values"], "status": "partial", "evidence": ["tests/corpus/5833-url-search-params-iterators.ts", "tests/corpus/5834-url-search-params-iterators-dynamic.mjs"] },
{ "chapter": "url", "signatures": ["Class: `URL`"], "status": "partial", "evidence": ["tests/corpus/url-runtime-base.ts", "tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`protocol` Type: {string}", "`username` Type: {string}", "`password` Type: {string}", "`host` Type: {string}", "`hostname` Type: {string}", "`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-authority.ts", "tests/corpus/node24-url-authority-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`href` Type: {string}", "`pathname` Type: {string}", "`search` Type: {string}", "`hash` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
{ "chapter": "url", "signatures": ["`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/2946-url-port-hash.ts"] },
{ "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] },
+ { "chapter": "test", "parent": "`context.assert`", "symbols": ["context.assert.snapshot", "context.assert.fileSnapshot"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/snapshot.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] },
+ { "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
+ { "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
@@ -359,7 +374,7 @@
{ "symbols": ["console.log", "console.info", "console.debug", "console.error", "console.warn"], "status": "partial", "evidence": ["tests/corpus/1460-console-error-warn.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2631-create-require/main.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"] },
{ "symbols": ["console.count", "console.countReset", "console.group", "console.groupCollapsed", "console.groupEnd"], "status": "partial", "evidence": ["tests/corpus/5095-console-count-group.ts", "tests/corpus/5098-console-time-warnings.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] },
{ "symbols": ["console.time", "console.timeEnd", "console.timeLog"], "status": "partial", "evidence": ["tests/corpus/5098-console-time-warnings.ts"] },
- { "symbols": ["console.assert", "console.dir", "console.dirxml"], "status": "partial", "evidence": ["tests/corpus/5096-console-assert-dir.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] },
+ { "symbols": ["console.assert", "console.dir", "console.dirxml"], "status": "partial", "evidence": ["tests/corpus/5096-console-assert-dir.ts", "tests/corpus/5099-console-methods-dynamic.cjs", "tests/corpus/5884-inspect-layout-options.ts"] },
{ "symbols": ["console.table"], "status": "partial", "evidence": ["tests/corpus/5097-console-table.ts", "tests/corpus/5099-console-methods-dynamic.cjs"] },
{ "symbols": ["console.clear", "console.profile", "console.profileEnd", "console.timeStamp"], "status": "partial", "evidence": ["tests/corpus/5099-console-methods-dynamic.cjs"] },
{ "symbols": ["assert.AssertionError"], "status": "partial", "evidence": ["tests/corpus/5510-assert-error-properties.js", "tests/corpus/5511-assert-error-class.ts", "tests/corpus/5512-assert-error-options.js"] },
@@ -374,13 +389,17 @@
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
- { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.getEventListeners", "events.listenerCount", "events.once", "events.on", "events.setMaxListeners"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
+ { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
+ { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] },
- { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts"] },
- { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts"] },
+ { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
+ { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
+ { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] },
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs"] },
{ "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"] },
{ "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] },
+ { "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
+ { "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
{ "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts"] },
{ "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
{ "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] }
diff --git a/native/llvm-codegen/README.md b/native/llvm-codegen/README.md
index 5b8f38f2a..8debaeeaf 100644
--- a/native/llvm-codegen/README.md
+++ b/native/llvm-codegen/README.md
@@ -35,6 +35,8 @@ requested output.
Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host.
+WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
+
`--cache-dir ` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect.
Unsanitized `speed` program builds, and unsanitized `release` builds of programs small enough for one partition, pass `--import-bitcode ` once per runtime-pack unit the program links that ships bitcode. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted.
diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp
index b39d2c1bf..a5f87e4b6 100644
--- a/native/llvm-codegen/src/emit.cpp
+++ b/native/llvm-codegen/src/emit.cpp
@@ -10,9 +10,15 @@
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
+#include "llvm/Analysis/RuntimeLibcallInfo.h"
+#include "llvm/Analysis/TargetLibraryInfo.h"
#include "llvm/Bitcode/BitcodeReader.h"
#include "llvm/Bitcode/BitcodeWriter.h"
#include "llvm/Bitcode/BitcodeWriterPass.h"
+#include "llvm/CodeGen/MachineDominators.h"
+#include "llvm/CodeGen/MachineFunction.h"
+#include "llvm/CodeGen/MachineFunctionPass.h"
+#include "llvm/CodeGen/MachineModuleInfo.h"
#include "llvm/IR/DiagnosticInfo.h"
#include "llvm/IR/DiagnosticPrinter.h"
#include "llvm/IR/LegacyPassManager.h"
@@ -20,6 +26,9 @@
#include "llvm/IR/Verifier.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/LTO/LTO.h"
+#include "llvm/Pass.h"
+#include "llvm/PassInfo.h"
+#include "llvm/PassRegistry.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/Caching.h"
#include "llvm/Support/FileSystem.h"
@@ -140,6 +149,138 @@ std::optional verify(Module &M, StringRef Code) {
return std::nullopt;
}
+// Runs WebAssembly's pass that gives multiple-entry loops a single entry only
+// on functions that have such a loop. The pass first records, for every
+// block, every block it can reach, so its time and memory grow with the
+// square of the block count even when it changes nothing: a module
+// initializer of 40,000 blocks took four minutes and 22 GB in it. It changes
+// a function only when two loop entries can reach each other, which a
+// reducible control flow graph never has, so skipping reducible functions
+// leaves the output unchanged.
+class IrreducibleControlFlowOnly final : public MachineFunctionPass {
+public:
+ static char ID;
+
+ explicit IrreducibleControlFlowOnly(FunctionPass *Fix)
+ : MachineFunctionPass(ID), Fix(Fix) {}
+
+ StringRef getPassName() const override { return Fix->getPassName(); }
+
+ bool doInitialization(Module &M) override {
+ MachineFunctionPass::doInitialization(M);
+ return Fix->doInitialization(M);
+ }
+
+ bool doFinalization(Module &M) override { return Fix->doFinalization(M); }
+
+ void getAnalysisUsage(AnalysisUsage &Usage) const override {
+ Fix->getAnalysisUsage(Usage);
+ }
+
+ bool runOnMachineFunction(MachineFunction &MF) override {
+ if (isReducible(MF))
+ return false;
+ // The wrapped pass reads the machine function through the module's
+ // machine code analysis, which this pass requires on its behalf.
+ if (Fix->getResolver() == nullptr) {
+ auto *Resolver = new AnalysisResolver(getResolver()->getPMDataManager());
+ Resolver->addAnalysisImplsPair(
+ &MachineModuleInfoWrapperPass::ID,
+ &getAnalysis());
+ Fix->setResolver(Resolver);
+ }
+ return Fix->runOnFunction(MF.getFunction());
+ }
+
+private:
+ // A graph is reducible when every edge that a depth-first search finds
+ // going back to a block on its stack targets a block that dominates the
+ // edge's source. Blocks the entry cannot reach count as irreducible, since
+ // the wrapped pass also examines them.
+ static bool isReducible(MachineFunction &MF) {
+ MachineDominatorTree Dominators(MF);
+ enum class State : unsigned char { New, Active, Done };
+ std::vector States(MF.getNumBlockIDs(), State::New);
+ SmallVector,
+ 32>
+ Stack;
+ size_t Visited = 1;
+ MachineBasicBlock *Entry = &MF.front();
+ States[Entry->getNumber()] = State::Active;
+ Stack.push_back({Entry, Entry->succ_begin()});
+ while (!Stack.empty()) {
+ auto &[Block, Next] = Stack.back();
+ if (Next == Block->succ_end()) {
+ States[Block->getNumber()] = State::Done;
+ Stack.pop_back();
+ continue;
+ }
+ MachineBasicBlock *Successor = *Next++;
+ State &Seen = States[Successor->getNumber()];
+ if (Seen == State::Active) {
+ if (!Dominators.dominates(Successor, Block))
+ return false;
+ } else if (Seen == State::New) {
+ Seen = State::Active;
+ ++Visited;
+ Stack.push_back({Successor, Successor->succ_begin()});
+ }
+ }
+ return Visited == MF.size();
+ }
+
+ std::unique_ptr Fix;
+};
+
+char IrreducibleControlFlowOnly::ID = 0;
+
+// The code generation pipeline for one module, with two changes for
+// WebAssembly. The irreducible control flow pass runs only where it can
+// change the function (see IrreducibleControlFlowOnly). Object emission also
+// leaves out the last machine pass, a module pass that records the external
+// symbols every function uses so that assembly output can declare them
+// before the first function. A module pass in the middle of the pipeline
+// keeps the machine code of every function alive until all of them are
+// compiled. Object files do not contain those declarations, and the printer
+// creates each symbol when it lowers the first use, so without the pass each
+// function's machine code is freed once it is printed.
+class CodeGenPasses final : public legacy::PassManagerBase {
+public:
+ CodeGenPasses(const TargetMachine &Machine, CodeGenFileType Type)
+ : Wasm(Machine.getTargetTriple().isWasm()),
+ Object(Type == CodeGenFileType::ObjectFile) {}
+
+ void add(Pass *P) override {
+ if (Wasm) {
+ StringRef Argument = passArgument(*P);
+ if (Argument == "wasm-fix-irreducible-control-flow") {
+ Passes.add(
+ new IrreducibleControlFlowOnly(static_cast(P)));
+ return;
+ }
+ if (Object && Argument == "wasm-mclower-prepass") {
+ delete P;
+ return;
+ }
+ }
+ Passes.add(P);
+ }
+
+ void run(Module &M) { Passes.run(M); }
+
+private:
+ static StringRef passArgument(const Pass &P) {
+ const PassInfo *Info =
+ PassRegistry::getPassRegistry()->getPassInfo(P.getPassID());
+ return Info == nullptr ? StringRef() : Info->getPassArgument();
+ }
+
+ bool Wasm;
+ bool Object;
+ legacy::PassManager Passes;
+};
+
// Writes code for an optimized module to a unique sibling of OutputPath,
// which is appended to Temporaries once it exists.
std::optional
@@ -153,7 +294,7 @@ generateCode(Module &M, TargetMachine &Machine, CodeGenFileType Type,
return EmitFailure{"output_open_failed", EC.message()};
Temporaries.push_back(TemporaryPath);
raw_fd_ostream Output(TemporaryFd, true);
- legacy::PassManager CodeGeneration;
+ CodeGenPasses CodeGeneration(Machine, Type);
if (Machine.addPassesToEmitFile(CodeGeneration, Output, nullptr, Type))
return EmitFailure{"emission_not_supported",
"target does not support the requested file type"};
@@ -325,7 +466,7 @@ compileUnoptimizedPartitions(const EmitOptions &Options,
P.PB.buildPerModuleDefaultPipeline(OptimizationLevel::O0)
.run(**Mod, P.MAM);
raw_svector_ostream Output(Objects[I]);
- legacy::PassManager CodeGeneration;
+ CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
CodeGenFileType::ObjectFile))
return Failures.set({"emission_not_supported",
@@ -437,6 +578,44 @@ compileOptimizedPartitions(const EmitOptions &Options,
return true;
};
+ // The backend's own code generation cannot use CodeGenPasses, so
+ // WebAssembly partitions are compiled here with the passes it would add,
+ // and returning false skips it.
+ if (TargetTriple.isWasm())
+ Config.PreCodeGenModuleHook = [&](unsigned Task, const Module &Optimized) {
+ size_t I = Task - 1;
+ if (I >= Count)
+ return true;
+ std::string LookupError;
+ std::unique_ptr Machine =
+ createTargetMachine(Options.Target, Options.OptLevel, LookupError);
+ if (!Machine) {
+ Failures.set({"target_machine_failed", LookupError});
+ return false;
+ }
+ // The backend passes the module it owns; only the hook's signature is
+ // const.
+ Module &Mod = const_cast(Optimized);
+ Objects[I].clear();
+ raw_svector_ostream Output(Objects[I]);
+ CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
+ TargetLibraryInfoImpl LibraryInfo(Mod.getTargetTriple(),
+ Machine->Options.VecLib);
+ CodeGeneration.add(new TargetLibraryInfoWrapperPass(LibraryInfo));
+ CodeGeneration.add(new RuntimeLibraryInfoWrapper(
+ Mod.getTargetTriple(), Machine->Options.ExceptionModel,
+ Machine->Options.FloatABIType, Machine->Options.EABIVersion,
+ Machine->Options.MCOptions.ABIName, Machine->Options.VecLib));
+ if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
+ CodeGenFileType::ObjectFile)) {
+ Failures.set({"emission_not_supported",
+ "target does not support object emission"});
+ return false;
+ }
+ CodeGeneration.run(Mod);
+ return false;
+ };
+
// The link refers to module identifiers for its whole lifetime.
std::vector Names(Count);
for (size_t I = 0; I < Count; ++I)
diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json
index 1d63579dd..ff55c940e 100644
--- a/packages/cli-darwin-arm64/package.json
+++ b/packages/cli-darwin-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-arm64",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Native scriptc compiler for darwin-arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json
index 8ee9cb173..b3b0a58dc 100644
--- a/packages/cli-darwin-x64/package.json
+++ b/packages/cli-darwin-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-x64",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Native scriptc compiler for darwin-x64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json
index 7fc63f63d..ef9fe8bcc 100644
--- a/packages/cli-linux-arm64-gnu/package.json
+++ b/packages/cli-linux-arm64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-gnu",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Native scriptc compiler for linux-arm64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json
index 6705e67c7..a33eb60ab 100644
--- a/packages/cli-linux-arm64-musl/package.json
+++ b/packages/cli-linux-arm64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-musl",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Native scriptc compiler for linux-arm64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json
index c278b55ed..25cd08204 100644
--- a/packages/cli-linux-x64-gnu/package.json
+++ b/packages/cli-linux-x64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-gnu",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Native scriptc compiler for linux-x64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json
index ad5e47544..b07577400 100644
--- a/packages/cli-linux-x64-musl/package.json
+++ b/packages/cli-linux-x64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-musl",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Native scriptc compiler for linux-x64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json
index b128cd53a..23dc94994 100644
--- a/packages/cli-win32-x64-msvc/package.json
+++ b/packages/cli-win32-x64-msvc/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-win32-x64-msvc",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Native scriptc compiler for win32-x64-msvc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index d72b26cf4..434b76fe6 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "scriptc",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index 5af2aa487..8cb8c972f 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -708,6 +708,7 @@ interface MessageEvent extends Event {
readonly data: T;
readonly origin: string;
readonly lastEventId: string;
+ readonly source: unknown;
readonly ports: readonly MessagePort[];
}
declare var MessageEvent: {
@@ -1026,7 +1027,7 @@ declare module "node:timers/promises" {
options?: unknown,
): AsyncGenerator;
export const scheduler: {
- wait(delay?: number): Promise;
+ wait(delay?: number, options?: TimerOptions): Promise;
yield(): Promise;
};
}
@@ -2109,6 +2110,8 @@ declare module "os" {
export function version(): string;
export function endianness(): "BE" | "LE";
export function getPriority(pid?: number): number;
+ export function setPriority(priority: number): void;
+ export function setPriority(pid: number, priority: number): void;
/* The TARGET platform's null device ("\\\\.\\nul" under a win32 triple). */
export const devNull: string;
/* The platform's constants: null-prototype objects with read-only
@@ -3250,6 +3253,7 @@ declare module "util/types" {
export function isInt8Array(value: unknown): value is Int8Array;
export function isInt16Array(value: unknown): value is Int16Array;
export function isInt32Array(value: unknown): value is Int32Array;
+ export function isFloat16Array(value: unknown): value is Float16Array;
export function isFloat32Array(value: unknown): value is Float32Array;
export function isFloat64Array(value: unknown): value is Float64Array;
export function isMap(value: unknown): value is Map;
@@ -3264,6 +3268,21 @@ declare module "util/types" {
export function isGeneratorFunction(value: unknown): boolean;
export function isGeneratorObject(value: unknown): boolean;
export function isModuleNamespaceObject(value: unknown): boolean;
+ export function isDate(value: unknown): value is Date;
+ export function isSharedArrayBuffer(value: unknown): value is SharedArrayBuffer;
+ export function isMapIterator(value: unknown): boolean;
+ export function isSetIterator(value: unknown): boolean;
+ export function isKeyObject(value: unknown): boolean;
+ export function isNumberObject(value: unknown): boolean;
+ export function isStringObject(value: unknown): boolean;
+ export function isBooleanObject(value: unknown): boolean;
+ export function isBigIntObject(value: unknown): boolean;
+ export function isSymbolObject(value: unknown): boolean;
+ export function isBoxedPrimitive(value: unknown): boolean;
+ export function isBigInt64Array(value: unknown): value is BigInt64Array;
+ export function isBigUint64Array(value: unknown): value is BigUint64Array;
+ export function isCryptoKey(value: unknown): boolean;
+ export function isExternal(value: unknown): boolean;
}
declare module "node:util/types" {
export * from "util/types";
@@ -3319,7 +3338,12 @@ declare module "node:test" {
beforeEach(fn: HookFn): void;
afterEach(fn: HookFn): void;
}
+ interface AssertSnapshotOptions {
+ readonly serializers?: ReadonlyArray<(value: any) => any> | undefined;
+ }
interface TestContextAssert {
+ snapshot(value: any, options?: AssertSnapshotOptions): void;
+ fileSnapshot(value: any, path: string, options?: AssertSnapshotOptions): void;
ok(value: unknown, message?: string): void;
strictEqual(actual: unknown, expected: unknown, message?: string): void;
notStrictEqual(actual: unknown, expected: unknown, message?: string): void;
@@ -3410,6 +3434,19 @@ declare module "node:test" {
: never;
reset(): void;
restoreAll(): void;
+ readonly timers: MockTimers;
+ }
+ interface MockTimersOptions {
+ apis: ReadonlyArray<"setInterval" | "setTimeout" | "setImmediate" | "Date">;
+ now?: number | Date | undefined;
+ }
+ interface MockTimers {
+ enable(options?: MockTimersOptions): void;
+ setTime(milliseconds?: number): void;
+ reset(): void;
+ tick(milliseconds?: number): void;
+ runAll(): void;
+ [Symbol.dispose](): void;
}
const mock: MockTracker;
/* Declared for typechecking only — every use fences (no lowering).
@@ -3437,6 +3474,8 @@ declare module "node:test" {
MockFunctionContext,
MockFunctionCall,
MockTracker,
+ MockTimers,
+ MockTimersOptions,
};
export default test;
}
@@ -3618,7 +3657,8 @@ declare module "querystring" {
| string
| number
| boolean
- | ReadonlyArray
+ | bigint
+ | ReadonlyArray
| null
| undefined;
}
@@ -4436,6 +4476,48 @@ declare module "net" {
remoteFamily?: string;
}
export type AddressInfo = import("node:dgram").AddressInfo;
+ export type IPVersion = "ipv4" | "ipv6";
+ export interface SocketAddressInitOptions {
+ address?: string;
+ family?: IPVersion;
+ flowlabel?: number;
+ port?: number;
+ }
+ /* A native handle (scr_net.c); the getters read its parsed address. */
+ export class SocketAddress {
+ constructor(options?: SocketAddressInitOptions);
+ readonly address: string;
+ readonly family: IPVersion;
+ readonly port: number;
+ readonly flowlabel: number;
+ toJSON(): { address: string; port: number; family: IPVersion; flowlabel: number };
+ /* undefined when the input is not "[:]" */
+ static parse(input: string): SocketAddress | undefined;
+ static isSocketAddress(value: unknown): value is SocketAddress;
+ }
+ /* Address, range and subnet rules; check() matches IPv4 addresses
+ * against IPv6 rules through their IPv4-mapped form, as Node does. */
+ export class BlockList {
+ constructor();
+ readonly rules: readonly string[];
+ readonly size: number;
+ addAddress(address: string | SocketAddress, type?: IPVersion): void;
+ addAddresses(addresses: readonly (string | SocketAddress)[], type?: IPVersion): void;
+ addRange(start: string | SocketAddress, end: string | SocketAddress, type?: IPVersion): void;
+ addSubnet(net: string | SocketAddress, prefix: number, type?: IPVersion): void;
+ addCIDR(cidr: string): void;
+ addCIDRs(cidrs: readonly string[]): void;
+ removeAddress(address: string | SocketAddress, type?: IPVersion): void;
+ removeRange(start: string | SocketAddress, end: string | SocketAddress, type?: IPVersion): void;
+ removeSubnet(net: string | SocketAddress, prefix: number, type?: IPVersion): void;
+ removeCIDR(cidr: string): void;
+ clear(): void;
+ check(address: string | SocketAddress, type?: IPVersion): boolean;
+ toJSON(): string[];
+ fromJSON(data: string | readonly string[]): void;
+ static isBlockList(value: unknown): value is BlockList;
+ static readonly PRIVATE_RANGES: readonly string[];
+ }
/* Node's IPv4Reg and IPv6Reg tests: 4, 6, or 0 for isIP. */
export function isIP(input: string): number;
export function isIPv4(input: string): boolean;
@@ -5764,19 +5846,42 @@ declare module "worker_threads" {
export const isMainThread: boolean;
export const isInternalThread: boolean;
export const threadId: number;
+ export interface ResourceLimits {
+ maxYoungGenerationSizeMb?: number;
+ maxOldGenerationSizeMb?: number;
+ codeRangeSizeMb?: number;
+ stackSizeMb?: number;
+ }
+ /* {} in the main thread */
+ export const resourceLimits: ResourceLimits;
+ /* the Worker's name option, trimmed ("WorkerThread" by default); "" in the main thread */
+ export const threadName: string | null;
export interface WorkerOptions {
workerData?: unknown;
transferList?: readonly unknown[];
argv?: readonly unknown[];
name?: string;
+ resourceLimits?: ResourceLimits;
+ stdout?: boolean;
+ stderr?: boolean;
+ stdin?: boolean;
[option: string]: unknown;
}
export class Worker {
constructor(filename: string | URL, options?: WorkerOptions);
readonly threadId: number;
+ readonly resourceLimits?: ResourceLimits;
+ readonly threadName: string | null;
+ /* with the stdout/stderr options: the worker's writes to those streams */
+ readonly stdout: import("stream").Readable;
+ readonly stderr: import("stream").Readable;
+ /* with the stdin option: what the worker reads from process.stdin */
+ readonly stdin: import("stream").Writable | null;
+ /* terminate(), resolving with undefined once the worker exited */
+ [Symbol.asyncDispose](): Promise;
on(event: "online", listener: () => void): this;
on(event: "message", listener: (value: any) => void): this;
- on(event: "error", listener: (error: Error) => void): this;
+ on(event: "error", listener: (error: any) => 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;
@@ -5818,6 +5923,27 @@ declare module "worker_threads" {
readonly port2: MessagePort;
}
export function receiveMessageOnPort(port: MessagePort): { message: any } | undefined;
+ /* This thread's environment data; each new Worker starts from a clone. */
+ export function setEnvironmentData(key: any, value?: any): void;
+ export function getEnvironmentData(key: any): any;
+ /* A marked object cannot be listed in a transfer list. */
+ export function markAsUntransferable(object: any): void;
+ /* structuredClone and postMessage throw DataCloneError for a marked
+ * object, error or class instance */
+ export function markAsUncloneable(object: any): void;
+ export function isMarkedAsUntransferable(object: any): boolean;
+ /* Delivers each message to every other open channel with the name, in
+ * any thread, as a 'message' MessageEvent. */
+ export class BroadcastChannel extends EventTarget {
+ constructor(name: string);
+ readonly name: string;
+ onmessage: ((event: MessageEvent) => void) | null;
+ onmessageerror: ((event: MessageEvent) => void) | null;
+ postMessage(message: unknown): void;
+ close(): void;
+ ref(): this;
+ unref(): this;
+ }
export const parentPort: MessagePort | null;
export const workerData: any;
}
@@ -5963,6 +6089,12 @@ declare module "events" {
eventName: string,
options?: { signal?: AbortSignal },
): Promise;
+ /* The emitter's own listenerCount/getMaxListeners/setMaxListeners/
+ * listeners, over EventEmitter instances. */
+ export function listenerCount(emitter: EventEmitter, eventName: string | symbol): number;
+ export function getMaxListeners(emitter: EventEmitter): number;
+ export function setMaxListeners(n?: number, ...eventTargets: EventEmitter[]): void;
+ export function getEventListeners(emitter: EventEmitter, name: string): Array<() => void>;
}
export = EventEmitter;
}
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index fa18a1b60..6e5442dc6 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -1,6 +1,6 @@
{
"name": "@scriptc/compiler",
- "version": "0.3.5",
+ "version": "0.3.6",
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts
index 5fb408403..8972b9e70 100644
--- a/packages/compiler/src/backend/cycle-analysis.test.ts
+++ b/packages/compiler/src/backend/cycle-analysis.test.ts
@@ -513,6 +513,19 @@ test("a subclass constructor runs after its base constructor's escape", () => {
checkExact(mod, ["object:Base", "object:Sub"], ["link"]);
});
+test("a base without a constructor function lets nothing escape", () => {
+ // Instantiations of a generic class name the generic class as their base,
+ // which has no constructor function of its own.
+ const mod = module();
+ mod.classes = [
+ { name: "Chain", fields: [{ name: "next", type: link }], loc },
+ { name: "Chain%0", base: "Chain", fields: [{ name: "next", type: link }], loc },
+ ];
+ mod.unions = [{ id: "link", arms: [obj("Chain%0"), { kind: "nullT" }] }];
+ mod.functions.push(ctor("Chain%0", [setThis("Chain%0", "next", link)]));
+ checkExact(mod, []);
+});
+
test("field reads of this inside the constructor do not escape", () => {
const read: IrStmt = {
kind: "fieldSet",
diff --git a/packages/compiler/src/backend/cycle-mutability.ts b/packages/compiler/src/backend/cycle-mutability.ts
index 24d3c470f..c7d476527 100644
--- a/packages/compiler/src/backend/cycle-mutability.ts
+++ b/packages/compiler/src/backend/cycle-mutability.ts
@@ -216,8 +216,13 @@ export function computeCycleMutability(
escapePoints.set(name, point);
return point;
};
- const escapes = (name: string): boolean =>
- escapePoint(name) < (fnByName.get(`%${name}.constructor`)?.body.length ?? 1);
+ const escapes = (name: string): boolean => {
+ const fn = fnByName.get(`%${name}.constructor`);
+ // A program class without a constructor function runs no code at construction. Generic
+ // classes are such bases: their instantiations name them as their base.
+ if (fn === undefined && classByName.get(name)?.runtime !== true) return false;
+ return escapePoint(name) < (fn?.body.length ?? 1);
+ };
const ancestorEscaped = (name: string): boolean => {
for (let c = classByName.get(name)?.base; c !== undefined; c = classByName.get(c)?.base)
if (escapes(c)) return true;
diff --git a/packages/compiler/src/backend/llvm/byte-numbers.ts b/packages/compiler/src/backend/llvm/byte-numbers.ts
index d12ef08b9..c51d03744 100644
--- a/packages/compiler/src/backend/llvm/byte-numbers.ts
+++ b/packages/compiler/src/backend/llvm/byte-numbers.ts
@@ -182,7 +182,11 @@ export function emitByteNumber(
const integerType = bits <= 32 ? "i32" : "i64";
if (spec.write) {
let raw: string;
- if (spec.floating) {
+ if (spec.floating && bits === 16) {
+ host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`);
+ raw = B.tmp();
+ B.line(`${raw} = call zeroext i16 @scr_f16_from_f64(double ${value!.name})`);
+ } else if (spec.floating) {
let stored = value!.name;
if (bits === 32) {
stored = B.tmp();
@@ -228,7 +232,10 @@ export function emitByteNumber(
const raw = endianBits(host, loaded, spec.width, little);
fastResult = B.tmp();
if (spec.floating) {
- if (bits === 32) {
+ if (bits === 16) {
+ host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`);
+ B.line(`${fastResult} = call double @scr_f16_to_f64(i16 zeroext ${raw})`);
+ } else if (bits === 32) {
const float = B.tmp();
B.line(`${float} = bitcast i32 ${raw} to float`);
B.line(`${fastResult} = fpext float ${float} to double`);
diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.test.ts b/packages/compiler/src/backend/llvm/call-lifetimes.test.ts
index 2572a22fd..90909a024 100644
--- a/packages/compiler/src/backend/llvm/call-lifetimes.test.ts
+++ b/packages/compiler/src/backend/llvm/call-lifetimes.test.ts
@@ -723,3 +723,40 @@ test("to-dyn of a copied record projects it, while live and identity conversions
fn.body = [convert(cls), ret(num())];
expect(analyzeWith(copies).get(fn.name)).toBeUndefined();
});
+
+test("a parameter stored as its last use takes the owned convention", () => {
+ const cell: IrType = { kind: "object", className: "Cell" };
+ const self: IrType = { kind: "object", className: "Box" };
+ const store = (value: IrExpr): IrStmt => ({
+ kind: "fieldSet",
+ obj: ref("this", self),
+ className: "Box",
+ field: "item",
+ value,
+ loc,
+ });
+ const ctor = (body: IrStmt[]): IrFunction => ({
+ name: "%Box.constructor",
+ loc,
+ returnType: { kind: "void" },
+ params: [
+ { localId: "this", name: "this", type: self },
+ { localId: "item", name: "item", type: cell },
+ ],
+ locals: [local("this", self), local("item", cell)],
+ body,
+ });
+ // `this.item = item` last: callers hand over their reference.
+ expect(analyze(ctor([store(ref("item", cell))])).borrowed.get("%Box.constructor")).toEqual(
+ new Set([0]),
+ );
+ // A later read keeps the borrowed convention (the store retains a copy).
+ const later = ctor([
+ store(ref("item", cell)),
+ { kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc },
+ ]);
+ expect(analyze(later).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
+ // A last use that only passes the parameter on is not a store.
+ const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]);
+ expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
+});
diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.ts b/packages/compiler/src/backend/llvm/call-lifetimes.ts
index 72acc625e..65dd76de7 100644
--- a/packages/compiler/src/backend/llvm/call-lifetimes.ts
+++ b/packages/compiler/src/backend/llvm/call-lifetimes.ts
@@ -9,6 +9,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { borrowsStringInputs } from "./string-lifetimes.js";
import { borrowsMapReadInputs } from "./map-read-lifetimes.js";
import { LazyCaptures } from "./lazy-captures.js";
+import { storedLastUseRead } from "./local-extents.js";
interface ForwardedUse {
callee: string;
@@ -343,6 +344,7 @@ export function analyzeCallLifetimes(
const lazyCaptures = new LazyCaptures(functions);
const nodes = new Map();
const unsafe: Parameter[] = [];
+ const consumed: { fn: string; index: number }[] = [];
const result: CallLifetimes = {
parameters: new Map(),
locals: new Map(),
@@ -416,6 +418,7 @@ export function analyzeCallLifetimes(
// A captured parameter that nothing rebinds keeps its entry value; its
// environment box takes a reference of its own when it is created.
const unchangedCaptures = lazyCaptures.parameters(fn);
+ const walks = walkParameters.get(fn.name);
fn.params.forEach((param, index) => {
const local = locals.get(param.localId);
if (
@@ -424,8 +427,16 @@ export function analyzeCallLifetimes(
isRefCounted(param.type) &&
(stable(local) || unchangedCaptures.has(local.id)) &&
!uses.declarations.has(local.id)
- )
+ ) {
borrowed.add(index);
+ if (
+ !walks?.has(index) &&
+ !unchangedCaptures.has(local.id) &&
+ param.name !== "this" &&
+ consumedParameter(fn, param.localId)
+ )
+ consumed.push({ fn: fn.name, index });
+ }
});
if (borrowed.size > 0) result.borrowed.set(fn.name, borrowed);
nodes.set(
@@ -499,5 +510,33 @@ export function analyzeCallLifetimes(
result.locals.set(fn.name, safe);
result.projectedLocals.set(fn.name, projected);
}
+ // Consumed parameters take the owned convention, unless only projected.
+ for (const { fn, index } of consumed) {
+ if (result.parameters.get(fn)?.has(index) === true) continue;
+ const borrowed = result.borrowed.get(fn);
+ if (!borrowed) continue;
+ borrowed.delete(index);
+ if (borrowed.size === 0) result.borrowed.delete(fn);
+ }
return result;
}
+
+/** A parameter whose last use in the body stores it (a field, record or
+ * element store, a record literal field, a push) and moves its reference
+ * there (lastUseRead). Its owned convention lets a caller hand over a
+ * temporary or its own last use; a borrowed one would retain the stored
+ * copy while the caller releases its temporary right after the call. The
+ * read must be the only node of its kind in the function, like every
+ * moved read. */
+function consumedParameter(fn: IrFunction, localId: string): boolean {
+ const read = storedLastUseRead(fn.body, -1, localId, true);
+ if (read === null) return false;
+ let count = 0;
+ const expr = (e: IrExpr): boolean => {
+ if (e === read) count++;
+ return everyExprChild(e, expr, stmt);
+ };
+ const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
+ fn.body.every(stmt);
+ return count === 1;
+}
diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts
index 0a473e65b..8a8778919 100644
--- a/packages/compiler/src/backend/llvm/common.ts
+++ b/packages/compiler/src/backend/llvm/common.ts
@@ -159,6 +159,7 @@ export const BYTES_ELEM_NUM: Record = {
u16: 6,
i16: 7,
u8c: 8,
+ f16: 9,
};
/** The payload marker of an ABSENT record field slot (IR fieldAbsent): the
diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts
index cc53d65b4..071d8e1ff 100644
--- a/packages/compiler/src/backend/llvm/constant-callbacks.test.ts
+++ b/packages/compiler/src/backend/llvm/constant-callbacks.test.ts
@@ -156,6 +156,93 @@ test("global closure aliases resolve without borrowing the caller's local namesp
entry.body.push({ kind: "assign", localId: "%g.callback", value: closure("predicate"), loc });
expect(facts(mod).get("consume")?.has("test")).not.toBe(true);
});
+test("recursion that forwards the unchanged callback agrees with the outer calls", () => {
+ const mod = fixture();
+ const consumer = mod.functions[1]!;
+ const recurse: IrStmt = {
+ kind: "exprStmt",
+ expr: { kind: "call", callee: "consume", args: [ref("test")], type: BOOL, loc },
+ loc,
+ };
+ consumer.body.unshift(recurse);
+ expect(facts(mod).get("consume")?.get("test")).toBe("predicate");
+ // Forwarding another function's callback is an ordinary incoming call.
+ consumer.body[0] = {
+ kind: "exprStmt",
+ expr: { kind: "call", callee: "consume", args: [closure("other")], type: BOOL, loc },
+ loc,
+ };
+ mod.functions.push({ ...mod.functions[0]!, name: "other" });
+ expect(facts(mod).get("consume")?.has("test")).not.toBe(true);
+});
+test("a callback forwarded through another consumer's parameter resolves", () => {
+ const mod = fixture();
+ const entry = mod.functions[2]!;
+ const forward: IrFunction = {
+ name: "forward",
+ ownsPrototype: true,
+ params: [{ localId: "f", name: "f", type: callback }],
+ locals: [{ id: "f", name: "f", type: callback, mutable: true }],
+ returnType: BOOL,
+ body: [
+ {
+ kind: "return",
+ value: { kind: "call", callee: "consume", args: [ref("f")], type: BOOL, loc },
+ loc,
+ },
+ ],
+ loc,
+ };
+ mod.functions.push(forward);
+ entry.body = [
+ {
+ kind: "exprStmt",
+ expr: { kind: "call", callee: "forward", args: [closure("predicate")], type: BOOL, loc },
+ loc,
+ },
+ ];
+ expect(facts(mod).get("consume")?.get("test")).toBe("predicate");
+ // A forwarder that rebinds its parameter forwards an unknown value.
+ forward.body.unshift({ kind: "assign", localId: "f", value: closure("other"), loc });
+ mod.functions.push({ ...mod.functions[0]!, name: "other" });
+ expect(facts(mod).get("consume")?.has("test")).not.toBe(true);
+});
+test("a closure capturing an unchanged callback parameter calls its target", () => {
+ const mod = fixture();
+ const consumer = mod.functions[1]!;
+ const inner: IrFunction = {
+ name: "%inner",
+ params: [],
+ captures: [{ localId: "c", name: "c", type: callback }],
+ locals: [{ id: "c", name: "c", type: callback, mutable: true, boxed: true }],
+ returnType: BOOL,
+ body: [
+ {
+ kind: "return",
+ value: {
+ kind: "callValue",
+ callee: ref("c"),
+ args: [{ kind: "numLit", value: 2, type: F64, loc }],
+ type: BOOL,
+ loc,
+ },
+ loc,
+ },
+ ],
+ loc,
+ };
+ mod.functions.push(inner);
+ consumer.locals[0]!.boxed = true;
+ consumer.body.unshift({
+ kind: "exprStmt",
+ expr: { kind: "closure", fnName: "%inner", captures: ["test"], type: funcOf([], BOOL), loc },
+ loc,
+ });
+ expect(facts(mod).get("%inner")?.get("c")).toBe("predicate");
+ // A write through the environment rebinds the shared binding.
+ inner.body.unshift({ kind: "assign", localId: "c", value: closure("predicate"), loc });
+ expect(facts(mod).get("%inner")?.has("c")).not.toBe(true);
+});
test("receiver elision admits only synchronous receiver-free work", () => {
const fn = fixture().functions[0]!;
expect(callbackIgnoresReceiver(fn)).toBe(true);
diff --git a/packages/compiler/src/backend/llvm/constant-callbacks.ts b/packages/compiler/src/backend/llvm/constant-callbacks.ts
index d77b9af43..6e6155460 100644
--- a/packages/compiler/src/backend/llvm/constant-callbacks.ts
+++ b/packages/compiler/src/backend/llvm/constant-callbacks.ts
@@ -1,12 +1,17 @@
-import type { IrExpr, IrModule } from "../../ir/ir.js";
+import type { IrExpr, IrFunction, IrModule } from "../../ir/ir.js";
import { exprFunctionReference, moduleFunctionReferences } from "../../ir/function-references.js";
+import { writtenLocals } from "../../ir/callback-specialization.js";
import { everyStmtList } from "../../ir/traverse.js";
import type { CallLifetimes } from "./call-lifetimes.js";
/** Closed direct-call consumers can devirtualize a callback when every
* incoming call supplies the same function. Keep its evaluated environment,
* arguments and ordinary ownership; only the loaded code pointer changes.
- * Escaped, exported, recursive forwarding and unknown targets stay indirect. */
+ * A call that forwards the consumer's own unchanged parameter (recursion)
+ * agrees with the others; a parameter forwarded to another consumer, or
+ * captured unchanged by a closure, resolves through the incoming calls of
+ * the function that declares it. Escaped, exported, mutually recursive
+ * forwarding and unknown targets stay indirect. */
export function findConstantCallbacks(
mod: IrModule,
lifetimes: CallLifetimes,
@@ -39,6 +44,9 @@ export function findConstantCallbacks(
);
const globals = new Map();
const writes = new Set();
+ // Parameters and captures each function (or a closure sharing the
+ // binding) writes after entry.
+ const rebinds = writtenLocals(mod);
// A function named anywhere but as a direct call's callee can be entered
// by a generated adapter or the runtime, which a direct-call census cannot
// specialize: the entry, exports, class helpers and methods here, and
@@ -175,10 +183,18 @@ export function findConstantCallbacks(
const next = definitions.get(owner)?.get(id);
if (next) return resolve(owner, next, seen);
const fn = functions.get(owner);
+ const parameter = fn?.params.findIndex((param) => param.localId === id) ?? -1;
+ if (fn && parameter >= 0) return parameterTarget(fn, parameter, seen);
const index = fn?.captures?.findIndex((capture) => capture.localId === id) ?? -1;
const local = fn?.locals.find((candidate) => candidate.id === id);
const sites = closureSites.get(owner);
- if (index < 0 || !local || local.mutable || !sites?.length) return null;
+ if (
+ index < 0 ||
+ !local ||
+ (local.mutable && rebinds.get(owner)?.has(id) !== false) ||
+ !sites?.length
+ )
+ return null;
let target: string | null = null;
for (const site of sites) {
const outer = site.captures[index];
@@ -194,6 +210,40 @@ export function findConstantCallbacks(
}
return target;
}
+ // The one function every incoming call passes in a consumer's callback
+ // parameter that its body never rebinds. Results are cached: helpers
+ // forwarding one callback through several calls resolve it once.
+ const parameterTargets = new Map();
+ function parameterTarget(fn: IrFunction, index: number, seen: Set): string | null {
+ const param = fn.params[index]!;
+ const key = `%param:${fn.name}:${index}`;
+ const cached = parameterTargets.get(key);
+ if (cached !== undefined) return cached;
+ if (
+ seen.has(key) ||
+ param.type.kind !== "func" ||
+ !consumers.has(fn.name) ||
+ escaped.has(fn.name) ||
+ rebinds.get(fn.name)?.has(param.localId) !== false
+ )
+ return null;
+ seen.add(key);
+ const calls = incoming.get(fn.name) ?? [];
+ let target: string | null = null;
+ for (const call of calls) {
+ const value = call.args[index];
+ if (call.owner === fn.name && value?.kind === "varRef" && value.localId === param.localId)
+ continue;
+ const next = value ? resolve(call.owner, value, new Set(seen)) : null;
+ if (!next || (target && target !== next)) {
+ target = null;
+ break;
+ }
+ target = next;
+ }
+ parameterTargets.set(key, target);
+ return target;
+ }
// Global targets are module facts. Resolve them once, and copy only
// entries actually used as indirect callees in each body. A module-wide
// global/function cross product would dominate large-source emission.
@@ -207,7 +257,11 @@ export function findConstantCallbacks(
const needed = indirectCallees.get(fn.name)!;
if (!needed.size) continue;
const targets = new Map();
+ const params = new Set(fn.params.map((param) => param.localId));
for (const id of needed) {
+ // A parameter called in its own body resolves below, only where the
+ // body borrows it.
+ if (params.has(id)) continue;
if (localIds.get(fn.name)?.has(id)) {
const target = resolveLocal(fn.name, id, new Set());
if (target) targets.set(id, target);
@@ -228,16 +282,7 @@ export function findConstantCallbacks(
: lifetimes.borrowed.get(fn.name)?.has(index))
)
return;
- let target: string | null = null;
- for (const call of calls) {
- const value = call.args[index];
- const next = value ? resolve(call.owner, value) : null;
- if (!next || (target && target !== next)) {
- target = null;
- break;
- }
- target = next;
- }
+ const target = parameterTarget(fn, index, new Set());
if (target) targets.set(param.localId, target);
});
}
diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts
index dc23ceae2..31680a518 100644
--- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts
+++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts
@@ -3,6 +3,7 @@ import {
BOOL,
F64,
STRING,
+ UNDEFINED_T,
VOID,
arrayOf,
type IrExpr,
@@ -23,11 +24,17 @@ const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({
});
const index = ref("i", F64);
-function module(name: string, returnType: IrExpr["type"], body: IrStmt[]): IrModule {
+function module(
+ name: string,
+ returnType: IrExpr["type"],
+ body: IrStmt[],
+ unions: IrModule["unions"] = [],
+): IrModule {
return {
irVersion: 16,
sourceFile: loc.file,
entry: "__main",
+ unions,
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
{
@@ -174,6 +181,76 @@ test.each([32, 64] as const)(
},
);
+test.each([32, 64] as const)(
+ "pop removes the last element of a packed array inline and keeps a cold runtime pop (%i-bit)",
+ (bits) => {
+ const maybe = { kind: "union", unionId: "maybe" } as const;
+ const pop: IrExpr = {
+ kind: "arrIntrinsic",
+ method: "pop",
+ receiver: ref("a", nums),
+ args: [],
+ type: maybe,
+ loc,
+ };
+ const llvm = emitLlvmModule(
+ module(
+ "take",
+ maybe,
+ [{ kind: "return", value: pop, loc }],
+ [{ id: "maybe", arms: [F64, UNDEFINED_T] }],
+ ),
+ { pointerBits: bits },
+ );
+ const fn = body(llvm, "take");
+ const size = bits === 32 ? "i32" : "i64";
+ // An empty array answers undefined without a call.
+ expect(fn).toMatch(new RegExp(`icmp eq ${size} %t\\d+, 0\\n br i1 %t\\d+, label %remove\\.a`));
+ expect(fn).toMatch(new RegExp(`sub nuw ${size} %t\\d+, 1`));
+ expect(fn).toMatch(/store i64 0, ptr /);
+ expect(fn).toMatch(/store i8 0, ptr /);
+ expect(fn).toMatch(/phi i64 /);
+ expect(fn).toMatch(/call zeroext i8 @scr_arr_pop_state\(.*\) cold$/m);
+ },
+);
+
+test("a discarded pop releases the removed element inline", () => {
+ const words = ref("s", arrayOf(STRING));
+ const length: IrExpr = {
+ kind: "arrIntrinsic",
+ method: "length",
+ receiver: words,
+ args: [],
+ type: F64,
+ loc,
+ };
+ const llvm = emitLlvmModule(
+ module("drop", VOID, [
+ {
+ kind: "arraySetLength",
+ arr: words,
+ length: {
+ kind: "bin",
+ op: "-",
+ left: length,
+ right: { kind: "numLit", value: 1, type: F64, loc },
+ type: F64,
+ loc,
+ },
+ loc,
+ pop: true,
+ },
+ ]),
+ );
+ const fn = body(llvm, "drop");
+ expect(fn).toMatch(/fsub double %t\d+, 1\.0/);
+ expect(fn).toMatch(/load i64, ptr .*\n(?:.*\n)*? store i64 0, ptr /);
+ expect(fn).toMatch(/inttoptr i64 %t\d+ to ptr/);
+ expect(fn).toContain("call void @scr_str_release(");
+ // Holes, sparse storage, and every other length keep the runtime call.
+ expect(fn).toContain("call void @scr_arr_set_len(");
+});
+
test.each([32, 64] as const)(
"indices below the hole-free prefix bound skip the state bytes (%i-bit)",
(bits) => {
diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts
index aeaf3e70f..183bb8239 100644
--- a/packages/compiler/src/backend/llvm/dense-array-access.ts
+++ b/packages/compiler/src/backend/llvm/dense-array-access.ts
@@ -508,6 +508,75 @@ export function emitDenseArraySet(
B.startBlock(join);
}
+/** The dense half of `pop`. An empty array branches to `empty` (the
+ * runtime's pop returns a hole state there and changes nothing). When the
+ * hole-free prefix bound equals the length, the last element is a present
+ * value in dense storage, and the runtime's pop is exactly: clear its slot
+ * and state, lower the bound and the length to its index, and move the
+ * slot out. That block is left open and its slot bits are returned as an
+ * i64 (a boolean's 0 or 1, zero-extended); every other array branches to
+ * `slow`, which keeps the runtime call. */
+export function emitDenseArrayPop(
+ host: LlvmEmitterContext,
+ array: string,
+ acc: ArrayAccess,
+ empty: string,
+ slow: string,
+): string {
+ const B = host.B;
+ const size = host.sizeType;
+ const header = (field: number): string => {
+ const ptr = B.tmp();
+ B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
+ host.markMemoryPointer(ptr, "array:header");
+ return ptr;
+ };
+ const lenPtr = header(SCR_ARR_FIELD.len),
+ len = B.tmp(),
+ none = B.tmp();
+ B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
+ B.line(`${none} = icmp eq ${size} ${len}, 0`);
+ const check = B.newLabel("arr.pop.check"),
+ dense = B.newLabel("arr.pop.dense");
+ B.condBr(none, empty, check);
+ B.startBlock(check);
+ const filledPtr = header(SCR_ARR_FIELD.filled),
+ filled = B.tmp(),
+ packed = B.tmp();
+ B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
+ B.line(`${packed} = icmp eq ${size} ${sizedFilled(host, filled)}, ${len}`);
+ B.condBr(packed, dense, slow);
+ B.startBlock(dense);
+ const last = B.tmp();
+ B.line(`${last} = sub nuw ${size} ${len}, 1`);
+ const dataPtr = header(SCR_ARR_FIELD.data),
+ data = B.tmp(),
+ statesPtr = header(SCR_ARR_FIELD.present),
+ states = B.tmp();
+ B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
+ B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
+ const valuePtr = valuePointer(host, { offset: last, data }, acc);
+ const statePtr = statePointer(host, { offset: last, states });
+ const slotTy = slotType(acc);
+ let raw = B.tmp();
+ B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
+ if (slotTy === "i8") {
+ const wide = B.tmp();
+ B.line(`${wide} = zext i8 ${raw} to i64`);
+ raw = wide;
+ }
+ B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
+ B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
+ let last32 = last;
+ if (size === "i64") {
+ last32 = B.tmp();
+ B.line(`${last32} = trunc i64 ${last} to i32`);
+ }
+ B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
+ B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
+ return raw;
+}
+
/** `scr_arr_push_*` (the array takes the value's reference) returning the
* new length as a double. When `len < cap` the runtime's append is exactly
* a dense store at `len` plus `len + 1`: no sparse entry can exist past the
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index 845633148..86e97a374 100644
--- a/packages/compiler/src/backend/llvm/dyn.ts
+++ b/packages/compiler/src/backend/llvm/dyn.ts
@@ -1108,11 +1108,14 @@ export class LlDyn {
private readonly recordNameTables = new Map();
/** Targets the one-pass parser (scr_json_parse_schema) can build
- * directly: number, boolean and string leaves inside arrays and plain
- * records (no tuple, index signature, internal '%' slot or `__proto__`
- * member; 1..64 fields). Everything else keeps the checked-dynamic
- * route alone. */
- private jsonSchemaSupported(t: IrType, visiting: Set): boolean {
+ * directly: number, boolean and string leaves inside arrays and records
+ * (no tuple, internal '%' slot or `__proto__` member; at most 64 declared
+ * fields, and at least one unless an index signature collects the
+ * members), record members typed as unions of those leaves, records and
+ * arrays, null and undefined (optional and nullable members; see
+ * jsonUnionSupported), and index signatures whose value type is not a
+ * union. Everything else keeps the checked-dynamic route alone. */
+ private jsonSchemaSupported(t: IrType, visiting: Set, member = false): boolean {
switch (t.kind) {
case "f64":
case "bool":
@@ -1123,21 +1126,56 @@ export class LlDyn {
case "record": {
if (visiting.has(t.shapeId)) return true;
const shape = this.host.recordsById.get(t.shapeId);
- if (!shape || shape.tuple || shape.indexValue) return false;
- if (shape.fields.length === 0 || shape.fields.length > 64) return false;
+ if (!shape || shape.tuple) return false;
+ const index = shape.indexValue;
+ if (shape.fields.length > 64 || (shape.fields.length === 0 && !index)) return false;
visiting.add(t.shapeId);
- return shape.fields.every(
- (f) =>
- !f.name.startsWith("%") &&
- f.name !== "__proto__" &&
- this.jsonSchemaSupported(f.type, visiting),
+ return (
+ shape.fields.every(
+ (f) =>
+ !f.name.startsWith("%") &&
+ f.name !== "__proto__" &&
+ this.jsonSchemaSupported(f.type, visiting, true),
+ ) &&
+ (!index || this.jsonSchemaSupported(index, visiting))
);
}
+ case "union":
+ return member && this.jsonUnionSupported(t, visiting);
default:
return false;
}
}
+ /** A union record member the one-pass parser writes directly: every arm
+ * a number, boolean, string, null, undefined, or a supported record or
+ * array, with at most one record and one array arm and no discriminant.
+ * Each JSON value kind then selects at most one arm, so the checked
+ * route's first full match is that arm, and a value that arm rejects
+ * fails every arm. */
+ private jsonUnionSupported(t: IrType & { kind: "union" }, visiting: Set): boolean {
+ const def = this.host.unionsById.get(t.unionId);
+ if (!def || def.discriminant) return false;
+ let records = 0,
+ arrays = 0;
+ return def.arms.every((arm) => {
+ switch (arm.kind) {
+ case "f64":
+ case "bool":
+ case "string":
+ case "nullT":
+ case "undefinedT":
+ return true;
+ case "record":
+ return ++records === 1 && this.jsonSchemaSupported(arm, visiting);
+ case "array":
+ return ++arrays === 1 && this.jsonSchemaSupported(arm, visiting);
+ default:
+ return false;
+ }
+ });
+ }
+
/** The ScrJsonSchema constant for t (scr_runtime.h layout). Records list
* fields in declaration order — the order JSON.stringify writes — so the
* runtime's next-field guess usually matches without a scan. */
@@ -1155,7 +1193,7 @@ export class LlDyn {
case "bool":
case "string": {
const kind = t.kind === "f64" ? 0 : t.kind === "bool" ? 1 : 2;
- body = `${S} ${kind}, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null`;
+ body = `${S} ${kind}, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null, ptr null, ${S} 0`;
break;
}
case "array": {
@@ -1169,7 +1207,7 @@ export class LlDyn {
`}`,
``,
);
- body = `${S} 4, ${S} 0, ptr null, ptr @${elem}, ptr null, ptr @${ctor}, ptr ${vAdapters(host, t).release}`;
+ body = `${S} 4, ${S} 0, ptr null, ptr @${elem}, ptr null, ptr @${ctor}, ptr ${vAdapters(host, t).release}, ptr null, ${S} 0`;
break;
}
case "record": {
@@ -1190,24 +1228,112 @@ export class LlDyn {
const offset = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${index.get(fieldName)}) to ${S})`;
return `${fieldTy} { ptr ${host.cstr(fieldName)}, ${S} ${Buffer.byteLength(fieldName, "utf8")}, ${S} ${offset}, ptr @${sub} }`;
});
- const fields = `${name}_fields`;
- this.defs.push(
- `@${fields} = internal constant [${entries.length} x ${fieldTy}] [${entries.join(", ")}]`,
- ``,
- );
- body = `${S} 3, ${S} ${entries.length}, ptr @${fields}, ptr null, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}`;
+ let fields = "null";
+ if (entries.length > 0) {
+ fields = `@${name}_fields`;
+ this.defs.push(
+ `${fields} = internal constant [${entries.length} x ${fieldTy}] [${entries.join(", ")}]`,
+ ``,
+ );
+ }
+ // An index signature's undeclared members go to the overflow map
+ // after the declared fields.
+ const overflow = shape.indexValue
+ ? {
+ elem: `@${this.jsonSchema(shape.indexValue)}`,
+ offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + base}) to ${S})`,
+ }
+ : { elem: "null", offset: "0" };
+ body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
break;
}
+ case "union":
+ body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`;
+ break;
default:
throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.kind}`);
}
this.defs.push(
- `@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr } { ${body} }`,
+ `@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`,
``,
);
return name;
}
+ /** The ScrJsonUnion descriptor of a union member (scr_json.h layout):
+ * its slot representation, the arm each JSON value kind selects, and the
+ * values the runtime stores for null and for a missing member. Boxed
+ * unions build their boxes through `_box`. */
+ private jsonUnionSchema(t: IrType & { kind: "union" }, name: string): string {
+ const host = this.host;
+ const S = host.sizeType;
+ const def = host.unionsById.get(t.unionId);
+ if (!def) throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.unionId}`);
+ const nullable = host.nullableUnions.has(t.unionId);
+ const inline = host.inlineUnions.record(t);
+ const repr = nullable ? 0 : inline ? 1 : 2;
+ // Indexed by SCR_JARM_*: null, number, boolean, string, object, array, absent.
+ const armKinds = ["nullT", "f64", "bool", "string", "record", "array", "undefinedT"];
+ const tags = armKinds.map((kind) => def.arms.findIndex((arm) => arm.kind === kind));
+ const arms = armKinds.map((_, i) =>
+ i >= 1 && i <= 5 && tags[i]! >= 0
+ ? `ptr @${this.jsonSchema(def.arms[tags[i]!]!)}`
+ : "ptr null",
+ );
+ const nullTag = tags[0]!;
+ const absentTag = tags[6]!;
+ const nullValue = !inline && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null";
+ const absentValue = !inline && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null";
+ let box = "null";
+ if (repr === 2) {
+ box = `@${name}_box`;
+ const lines = [`define internal ptr ${box}(i32 %tag, i64 %bits) ${FN_ATTRS} {`, `entry:`];
+ const cases: string[] = [];
+ const blocks: string[] = [];
+ def.arms.forEach((arm, tag) => {
+ if (arm.kind === "nullT" || arm.kind === "undefinedT") return;
+ cases.push(`i32 ${tag}, label %arm${tag}`);
+ blocks.push(`arm${tag}:`);
+ if (arm.kind === "f64") {
+ host.declare(`declare ptr @scr_union_new_f64(i32, double)`);
+ blocks.push(
+ ` %f${tag} = bitcast i64 %bits to double`,
+ ` %u${tag} = call ptr @scr_union_new_f64(i32 ${tag}, double %f${tag})`,
+ );
+ } else if (arm.kind === "bool") {
+ host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`);
+ blocks.push(
+ ` %b${tag} = trunc i64 %bits to i1`,
+ ` %u${tag} = call ptr @scr_union_new_bool(i32 ${tag}, i1 zeroext %b${tag})`,
+ );
+ } else {
+ const rc = vAdapters(host, arm);
+ host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
+ blocks.push(
+ ` %p${tag} = inttoptr i64 %bits to ptr`,
+ ` %u${tag} = call ptr @scr_union_new_ref(i32 ${tag}, ptr %p${tag}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
+ );
+ }
+ blocks.push(` ret ptr %u${tag}`);
+ });
+ this.defs.push(
+ ...lines,
+ ` switch i32 %tag, label %none [ ${cases.join(" ")} ]`,
+ `none:`,
+ ` unreachable`,
+ ...blocks,
+ `}`,
+ ``,
+ );
+ }
+ const descriptor = `${name}_union`;
+ this.defs.push(
+ `@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box} }`,
+ ``,
+ );
+ return descriptor;
+ }
+
/** The declared-name tables scr_record_source_note scans: parallel
* constant arrays of field-name bytes and byte lengths in declaration
* order (the order the record serializes), interned per shape. Internal
@@ -2295,6 +2421,15 @@ export class LlDyn {
const B = new BlockBuilder();
let sourceAccessor: { name: string; release: string } | null = null;
switch (t.kind) {
+ case "date": {
+ // A Date handle holding the same time value; native Dates are
+ // immutable values, and so is the handle.
+ host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`);
+ const r = B.tmp();
+ B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`);
+ B.terminate(`ret ptr ${r}`);
+ break;
+ }
case "url":
case "searchParams": {
const native = t.kind === "url" ? "url" : "search_params";
@@ -3087,82 +3222,19 @@ export class LlDyn {
this.puts(B, "%b", "[object Object]");
B.br(done);
B.startBlock(lErr);
- const en = this.objGetLit(B, "%d", "name");
- const em = this.objGetLit(B, "%d", "message");
- // ens/ems: the member's ScrStr when it is a string, else null.
- const strOf = (m: string, hint: string): string => {
- const slot = B.slot();
- B.entryAllocas.push(`${slot} = alloca ptr`);
- B.line(`store ptr null, ptr ${slot}`);
- const nn = B.tmp();
- B.line(`${nn} = icmp ne ptr ${m}, null`);
- const lt = B.newLabel(`${hint}.t`);
- const lj = B.newLabel(`${hint}.j`);
- B.condBr(nn, lt, lj);
- B.startBlock(lt);
- const k = this.kindOf(B, m);
- const isStr = B.tmp();
- B.line(`${isStr} = icmp eq i32 ${k}, ${DYN_KIND.STR}`);
- const ls = B.newLabel(`${hint}.s`);
- B.condBr(isStr, ls, lj);
- B.startBlock(ls);
- const sv = this.payloadOf(B, m, "ptr");
- B.line(`store ptr ${sv}, ptr ${slot}`);
- B.br(lj);
- B.startBlock(lj);
- const out = B.tmp();
- B.line(`${out} = load ptr, ptr ${slot}`);
- return out;
- };
- const ens = strOf(en, "ds.en");
- const ems = strOf(em, "ds.em");
- const putIf = (s: string, hint: string): void => {
- const nn = B.tmp();
- B.line(`${nn} = icmp ne ptr ${s}, null`);
- const lt = B.newLabel(`${hint}.t`);
- const lj = B.newLabel(`${hint}.j`);
- B.condBr(nn, lt, lj);
- B.startBlock(lt);
- this.putScrStr(B, "%b", s);
- B.br(lj);
- B.startBlock(lj);
- };
- putIf(ens, "ds.pn");
- {
- // if (ens && ens->len && ems && ems->len) ": "
- const lenNz = (s: string, hint: string): string => {
- const slot = B.slot();
- B.entryAllocas.push(`${slot} = alloca i1`);
- B.line(`store i1 false, ptr ${slot}`);
- const nn = B.tmp();
- B.line(`${nn} = icmp ne ptr ${s}, null`);
- const lt = B.newLabel(`${hint}.t`);
- const lj = B.newLabel(`${hint}.j`);
- B.condBr(nn, lt, lj);
- B.startBlock(lt);
- const { len } = this.strParts(B, s);
- const nz = B.tmp();
- B.line(`${nz} = icmp ne ${this.S} ${len}, 0`);
- B.line(`store i1 ${nz}, ptr ${slot}`);
- B.br(lj);
- B.startBlock(lj);
- const out = B.tmp();
- B.line(`${out} = load i1, ptr ${slot}`);
- return out;
- };
- const a = lenNz(ens, "ds.ln");
- const b2 = lenNz(ems, "ds.lm");
- const both = B.tmp();
- B.line(`${both} = and i1 ${a}, ${b2}`);
- const lt = B.newLabel("ds.cl");
- const lj = B.newLabel("ds.cj");
- B.condBr(both, lt, lj);
- B.startBlock(lt);
- this.puts(B, "%b", ": ");
- B.br(lj);
- B.startBlock(lj);
- }
- putIf(ems, "ds.pm");
+ // Error.prototype.toString, reading name and message through the
+ // prototype. Null leaves the exception its conversion threw pending.
+ host.declare(`declare ptr @scr_dyn_error_string(ptr)`);
+ host.declare(`declare void @scr_str_release(ptr)`);
+ const text = B.tmp();
+ B.line(`${text} = call ptr @scr_dyn_error_string(ptr %d)`);
+ const written = B.tmp();
+ B.line(`${written} = icmp ne ptr ${text}, null`);
+ const lPut = B.newLabel("ds.ep");
+ B.condBr(written, lPut, done);
+ B.startBlock(lPut);
+ this.putScrStr(B, "%b", text);
+ B.line(`call void @scr_str_release(ptr ${text})`);
B.br(done);
}
B.startBlock(labels.get(DYN_KIND.BYTES)!);
@@ -3593,6 +3665,21 @@ export class LlDyn {
B.terminate(`ret ptr ${r}`);
B.startBlock(lNext);
}
+ // PROMISE: Promise.prototype.then/catch/finally as function values;
+ // every other key is undefined (promises have no own properties).
+ {
+ const isP = B.tmp();
+ B.line(`${isP} = icmp eq i32 ${kd}, ${DYN_KIND.PROMISE}`);
+ const lP = B.newLabel("kg.p");
+ const lNext = B.newLabel("kg.n");
+ B.condBr(isP, lP, lNext);
+ B.startBlock(lP);
+ host.declare(`declare ptr @scr_dyn_promise_key_get(ptr, ptr)`);
+ const r = B.tmp();
+ B.line(`${r} = call ptr @scr_dyn_promise_key_get(ptr %d, ptr %k)`);
+ B.terminate(`ret ptr ${r}`);
+ B.startBlock(lNext);
+ }
// The canonical-index parse, shared by BYTES/ARR/STR: digits only,
// no leading zero. Leaves (digits, idx).
const parseIndex = (): { digits: string; idx: string } => {
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index a9e840837..3fd8ec127 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -49,7 +49,7 @@ import {
STRING,
VOID,
} from "../../ir/ir.js";
-import { moduleRuntimeFeatures } from "../../ir/runtime-features.js";
+import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
@@ -89,6 +89,7 @@ import {
} from "./split-loops.js";
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { RESULT_ENTRY_PREFIX, addRecordResultEntries } from "./record-results.js";
+import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
import {
@@ -142,6 +143,7 @@ import {
mangleArgPack,
mangleAsyncAdoptCall,
mangleAsyncAwaitCall,
+ mangleAsyncInlineAwaitCall,
mangleAsyncSpawn,
mangleBorrowedFunction,
mangleClassObj,
@@ -197,7 +199,12 @@ import {
type LocalArrayRead,
} from "./local-array-reads.js";
import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js";
-import { emitDenseArrayGet, emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js";
+import {
+ emitDenseArrayGet,
+ emitDenseArrayPop,
+ emitDenseArraySet,
+ emitDenseSlotUnlink,
+} from "./dense-array-access.js";
import {
emitStackMapRead,
findMapReadLifetimes,
@@ -209,6 +216,7 @@ import { findMapEntryFusions, type MapEntryFusion } from "./map-entry-fusion.js"
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
import { analyzeWalks, findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
+import { lastUseRead } from "./local-extents.js";
import { findReboundParameters } from "./rebound-parameters.js";
import { ReferenceEffects } from "./reference-effects.js";
import { ReferenceWrites, type Writes } from "./reference-writes.js";
@@ -480,11 +488,14 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
options.debugSources === undefined
? addRecordResultEntries(
scalarizeNumericRecords(
- specializeNumericCalls(
- foldConstantGlobals(
- copyStableCaptures(
- hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
+ specializeCallbackCalls(
+ specializeNumericCalls(
+ foldConstantGlobals(
+ copyStableCaptures(
+ hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
+ ),
),
+ mangleFunction,
),
mangleFunction,
),
@@ -505,11 +516,14 @@ export function emitLlvmModuleSource(
options.debugSources === undefined
? addRecordResultEntries(
scalarizeNumericRecords(
- specializeNumericCalls(
- foldConstantGlobals(
- copyStableCaptures(
- hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
+ specializeCallbackCalls(
+ specializeNumericCalls(
+ foldConstantGlobals(
+ copyStableCaptures(
+ hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
+ ),
),
+ mangleFunction,
),
mangleFunction,
),
@@ -528,6 +542,18 @@ function llStrBytes(text: string): string {
}
const NO_BORROWED: ReadonlySet = new Set();
+
+/** Runtime entry points (and await-call entries) that may park the running
+ * execution context, as they appear in emitted function text. */
+const SUSPENDS =
+ /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_)|sc_a[wd]_)/;
+/** The suspensions of a fiber body's own awaits, which its stackless
+ * coroutine form suspends itself at instead. */
+const AWAIT_SUSPENSIONS = /@(?:scr_await_(?:void|f64|bool|str|ref|hop)\b|sc_a[wd]_[A-Za-z0-9_]+)/g;
+/** SUSPENDS in a stackless coroutine body, whose frame bookkeeping reads its
+ * own fiber and promise. */
+const COROUTINE_SUSPENDS =
+ /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_(?!self\b|promise\b))|sc_a[wd]_)/;
/** ScrExcKind tags of a generator slot holding a number or a boolean. */
const SCR_EXC_F64 = 1;
const SCR_EXC_BOOL = 2;
@@ -651,6 +677,12 @@ export class LlEmitter {
* generator stacks) and so do debug builds (the debugger reads locals
* from the native frame). */
private stacklessGenerators = false;
+ /** Async bodies that suspend only at their own awaits compile to the same
+ * switched coroutines on native targets (selectStacklessAsync), under the
+ * generators' conditions; library and Windows builds keep fibers. */
+ private stacklessAsyncAllowed = false;
+ /** The async functions selectStacklessAsync lowered as coroutines. */
+ private readonly stacklessAsync = new Set();
/** Some function in the module is lowered as a coroutine. */
private usesCoroutines = false;
/** The target lacks a rounding instruction at its baseline CPU (x86-64
@@ -837,6 +869,12 @@ export class LlEmitter {
/** Async functions returned directly at some `return f(...)` site of an
* async function; each gets an adopting entry (emitAsyncAwaitCall). */
readonly adoptCallees = new Set();
+ /** Fiberless async functions a stackless coroutine body awaits directly;
+ * each gets an inline await entry (emitCoroInlineAwait). */
+ readonly inlineAwaitCallees = new Set();
+ /** The async functions whose bodies never suspend and that a direct
+ * await may enter in place (selectStacklessAsync). */
+ private inlineAwaitable = new Set();
/** The promise a `return` emitted past its finally regions already
* produced for its adopting await (borrowed from the return's parked
* slot); that await takes it instead of evaluating its operand. */
@@ -850,6 +888,9 @@ export class LlEmitter {
/** setTimeout and friends appeared somewhere: main must run the event
* loop even in programs with no async functions. */
usesTimers = false;
+ /** The program calls mock.timers members (moduleUsesMockTimers): timer
+ * calls emit scr_test.c's mock-aware entry points. */
+ readonly mockTimers: boolean;
/** Emitted ref-kind resolve thunks for new Promise, interned per inner
* typeKey → thunk symbol. */
readonly resolveThunks = new Map();
@@ -988,6 +1029,9 @@ export class LlEmitter {
* not a constructor (where they change), not async or a generator. */
private finalFieldBorrows = false;
integerRanges: IntegerRanges = new Map();
+ /** Number arrays of the current function whose every present element is
+ * an int32 (Int32[] contracts), or null when the module has none. */
+ int32Elements: ((arr: IrExpr) => boolean) | null = null;
/** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */
wrappingSums = new Set();
private byteWindowsEnabled = true;
@@ -1073,6 +1117,7 @@ export class LlEmitter {
}),
);
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
+ this.mockTimers = moduleUsesMockTimers(mod);
this.debug =
options.debugSources === undefined
? null
@@ -1147,6 +1192,10 @@ export class LlEmitter {
this.mayThrow = mt.fns;
this.workerEntryPolls = mt.workerEntryPolls ?? null;
this.stackChecks = mt.stackChecks ?? null;
+ // A stackless call's eager prefix borrows its caller's stack; the
+ // runtime moves deep prefix nests to a fresh stack using the guard,
+ // which Windows lacks.
+ this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows;
this.publishedTypes = computePublishedTypes(mod);
this.immutableFields = computeImmutableFields(mod);
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
@@ -1865,6 +1914,7 @@ export class LlEmitter {
fnDefs.push(def);
this.fnDefText.set(fn.name, def);
}
+ this.selectStacklessAsync(fnDefs);
const errorViews: string[] = [];
for (const property of ["name", "message"])
if (this.mod.functions.some((fn) => fn.name === `%error.${property}.read`)) {
@@ -2000,12 +2050,28 @@ export class LlEmitter {
this.dyn.toDynHelper({ kind: "object", className: "%Writable" }),
]
: null;
- if (childStreamBoxes) {
+ // worker.stdout and worker.stderr box the stream unit's Readable the
+ // same way, and worker.stdin its Writable when the program has one
+ // (scr_worker_stdio_install, scr_worker_events.c).
+ const workerStdioBox =
+ this.mod.workers === true && usesStream && this.classMeta.has("%Readable")
+ ? (childStreamBoxes?.[0] ??
+ this.dyn.toDynHelper({ kind: "object", className: "%Readable" }))
+ : null;
+ const workerStdinBox =
+ this.mod.workers === true && usesStream && this.classMeta.has("%Writable")
+ ? (childStreamBoxes?.[1] ??
+ this.dyn.toDynHelper({ kind: "object", className: "%Writable" }))
+ : null;
+ if (childStreamBoxes || workerStdioBox || workerStdinBox) {
this.declare(
`@scr_stream_child_ops = external constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr }`,
);
- this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`);
}
+ if (childStreamBoxes)
+ this.declare(`declare void @scr_child_dyn_streams_install(ptr, ptr, ptr)`);
+ if (workerStdioBox || workerStdinBox)
+ this.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`);
if (usesFsWatch) this.declare(`declare void @scr_watch_install()`);
if (this.ffiHasForeignCallback) this.declare(`declare void @scr_ffi_install()`);
if (usesStream) this.declare(`declare void @scr_stream_install()`);
@@ -2018,7 +2084,10 @@ export class LlEmitter {
if (usesHttp) this.declare(`declare void @scr_http_dyn_install()`);
if (usesFetch) this.declare(`declare void @scr_fetch_install()`);
- if (usesNodeTest) this.declare(`declare i32 @scr_test_exit_code()`);
+ if (usesNodeTest) {
+ this.declare(`declare i32 @scr_test_exit_code()`);
+ this.declare(`declare void @scr_test_init()`);
+ }
if (snapshotsTlsCa) {
this.declare(`declare void @scr_tls_ca_install()`);
}
@@ -2162,6 +2231,12 @@ export class LlEmitter {
if (this.mod.lib.exports.some((e) => e.returns === "bytes")) {
this.declare(`declare void @scr_library_bytes_out(ptr, ptr, ptr)`);
}
+ if (this.mod.lib.exports.some((e) => e.params.includes("cstring"))) {
+ this.declare(`declare ptr @scr_library_cstr_in(ptr, ptr)`);
+ }
+ if (this.mod.lib.exports.some((e) => e.returns === "cstring")) {
+ this.declare(`declare ptr @scr_library_cstr_out(ptr)`);
+ }
}
// Helpers assemble BEFORE the declaration table flushes (they add
// write/abort declarations).
@@ -2595,6 +2670,11 @@ export class LlEmitter {
` call void @scr_child_dyn_streams_install(ptr @scr_stream_child_ops, ptr @${childStreamBoxes[0]}, ptr @${childStreamBoxes[1]})`,
]
: []),
+ ...(workerStdioBox || workerStdinBox
+ ? [
+ ` call void @scr_worker_stdio_install(ptr @scr_stream_child_ops, ptr ${workerStdioBox ? `@${workerStdioBox}` : "null"}, ptr ${workerStdinBox ? `@${workerStdinBox}` : "null"})`,
+ ]
+ : []),
// fs.watch programs fill the loop's watch hooks the same way —
// scr_watch.c links only when this line is emitted.
...(usesFsWatch ? [` call void @scr_watch_install()`] : []),
@@ -2607,6 +2687,8 @@ export class LlEmitter {
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
...(usesStream ? [` call void @scr_stream_install()`] : []),
` call void @scr_lib_init(i32 %argc, ptr %argv)`,
+ // node:test takes Node's --test-update-snapshots out of argv first.
+ ...(usesNodeTest ? [` call void @scr_test_init()`] : []),
...(asyncEntry
? [` %top = call ptr @${mangleAsyncSpawn(this.mod.entry)}()`]
: [` call void @${mangleFunction(this.mod.entry)}()`]),
@@ -2938,6 +3020,15 @@ export class LlEmitter {
``,
);
}
+ if (e.inboundCstringTrap !== undefined) {
+ // The cstring-in helper's host-contract trap message (a NULL
+ // `const char *`): same assembled structured form and SC4012 code.
+ const trapBytes = Buffer.byteLength(e.inboundCstringTrap, "utf8");
+ out.push(
+ `@sc_lib_cstr_trap_${e.symbol} = internal constant [${trapBytes + 1} x i8] c"${llStrBytes(e.inboundCstringTrap)}"`,
+ ``,
+ );
+ }
e.params.forEach((cls, i) => {
switch (cls) {
case "f64":
@@ -2995,6 +3086,13 @@ export class LlEmitter {
);
args.push(`ptr %c${i}`);
break;
+ case "cstring":
+ params.push(`ptr %a${i}`);
+ body.push(
+ ` %c${i} = call ptr @scr_library_cstr_in(ptr %a${i}, ptr @sc_lib_cstr_trap_${e.symbol})`,
+ );
+ args.push(`ptr %c${i}`);
+ break;
}
});
if (e.returns === "string" || e.returns === "bytes") {
@@ -3059,6 +3157,16 @@ export class LlEmitter {
` ret void`,
);
break;
+ case "cstring":
+ // The result's arena copy, returned as `const char *`.
+ retType = "ptr";
+ body.push(
+ ` %r = call ptr ${target}(${callArgs})`,
+ ` call void @scr_library_check_exc()`,
+ ` %z = call ptr @scr_library_cstr_out(ptr %r)`,
+ ` ret ptr %z`,
+ );
+ break;
}
out.push(
`define ${retType} @${e.symbol}(${params.join(", ")}) ${FN_ATTRS} { ; library export ${e.fnName}`,
@@ -3720,29 +3828,49 @@ export class LlEmitter {
* cannot reorder execution undetected: the runtime traps when a context parks while a
* fiberless body is on its stack. */
private fiberlessAsyncFunctions(): Set {
+ const parks = this.parkingFunctions();
+ const eligible = parks === null ? new Set() : this.neverParkingAsync(parks);
+ for (const name of this.inlineAwaitCallees)
+ if (!eligible.has(name))
+ throw new InternalCompilerError(`llvm emitter bug: inline await of suspending ${name}`);
+ return eligible;
+ }
+
+ /** The async functions `parks` leaves fiberless (fiberlessAsyncFunctions). */
+ private neverParkingAsync(parks: ReadonlySet): Set {
const eligible = new Set();
- if (this.wasi || this.mod.workers === true) return eligible;
- const suspends =
- /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_)|sc_a[wd]_)/;
- for (const decl of this.decls) if (/@scr_test_sub\b/.test(decl)) return eligible;
- for (const text of this.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return eligible;
- const byMangled = new Map();
for (const fn of this.mod.functions) {
- byMangled.set(mangleFunction(fn.name), fn.name);
- byMangled.set(mangleBorrowedFunction(fn.name), fn.name);
+ if (fn.async !== true || fn.generator !== undefined) continue;
+ if (fn.asyncCacheGlobal !== undefined || fn.asyncCycleCacheGlobal !== undefined) continue;
+ if (fn.returnType.kind === "dyn" || parks.has(fn.name)) continue;
+ if (!this.fnDefText.has(fn.name) || this.stacklessAsync.has(fn.name)) continue;
+ eligible.add(fn.name);
}
+ return eligible;
+ }
+
+ /** The module functions whose bodies may park their execution context:
+ * their emitted text mentions a suspension entry point (SUSPENDS), or
+ * they mention such a function (transitively). Null where async bodies
+ * must keep fibers regardless: wasm32 (its coroutine lowering owns async
+ * bodies), worker programs, and programs that use node:test subtests,
+ * whose `t.test()` awaits on the CURRENT fiber even from synchronous
+ * code. */
+ private parkingFunctions(): Set | null {
+ if (this.wasi || this.mod.workers === true) return null;
+ for (const decl of this.decls) if (/@scr_test_sub\b/.test(decl)) return null;
+ for (const text of this.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return null;
// Reverse mention edges, then propagate "may park" to every mentioner.
const mentionedBy = new Map>();
const parks = new Set();
const work: string[] = [];
for (const [name, text] of this.fnDefText) {
- if (suspends.test(text)) {
+ if (SUSPENDS.test(text)) {
parks.add(name);
work.push(name);
}
- for (const match of text.matchAll(/@(sc_b?f_[A-Za-z0-9_]+)/g)) {
- const callee = byMangled.get(match[1]!);
- if (callee === undefined || callee === name) continue;
+ for (const callee of this.mentionedFunctions(text)) {
+ if (callee === name) continue;
let callers = mentionedBy.get(callee);
if (callers === undefined) mentionedBy.set(callee, (callers = new Set()));
callers.add(name);
@@ -3757,14 +3885,67 @@ export class LlEmitter {
work.push(caller);
}
}
- for (const fn of this.mod.functions) {
- if (fn.async !== true || fn.generator !== undefined) continue;
- if (fn.asyncCacheGlobal !== undefined || fn.asyncCycleCacheGlobal !== undefined) continue;
- if (fn.returnType.kind === "dyn" || parks.has(fn.name)) continue;
- if (!this.fnDefText.has(fn.name)) continue;
- eligible.add(fn.name);
+ return parks;
+ }
+
+ private mangledFunctions: Map | null = null;
+
+ /** The module functions whose ordinary or borrowed body `text` mentions. */
+ private *mentionedFunctions(text: string): Generator {
+ if (this.mangledFunctions === null) {
+ this.mangledFunctions = new Map();
+ for (const fn of this.mod.functions) {
+ this.mangledFunctions.set(mangleFunction(fn.name), fn.name);
+ this.mangledFunctions.set(mangleBorrowedFunction(fn.name), fn.name);
+ }
}
- return eligible;
+ for (const match of text.matchAll(/@(sc_b?f_[A-Za-z0-9_]+)/g)) {
+ const callee = this.mangledFunctions.get(match[1]!);
+ if (callee !== undefined) yield callee;
+ }
+ }
+
+ /** Chooses the native async functions whose bodies compile to switched
+ * coroutines instead of running on a fiber: a suspended call then keeps a
+ * heap frame of the values live across its awaits rather than a fiber and
+ * its stack. A body qualifies when its only suspension points are its own
+ * awaits, which the coroutine form suspends itself at (emitCoroAwait), so
+ * nothing it runs can park the stack it borrows. It is first emitted as
+ * usual: that text may suspend only through await runtime calls and
+ * await-call entries, which the coroutine form replaces, and may call no
+ * function that parks. The coroutine form is then emitted and must pass
+ * the same test with no exceptions (its frame bookkeeping excepted);
+ * otherwise the fiber form stays. Bodies that never suspend stay
+ * fiberless, and module evaluation bodies and `dyn` results keep fibers
+ * as they do there. The runtime traps if a stackless body ever parks. */
+ private selectStacklessAsync(fnDefs: string[]): void {
+ if (!this.stacklessAsyncAllowed) return;
+ const parks = this.parkingFunctions();
+ if (parks === null) return;
+ // Coroutine bodies emitted below are the only inline-await callers, and
+ // replacing their text changes no other function's parking.
+ this.inlineAwaitable = this.neverParkingAsync(parks);
+ const qualifies = (fn: IrFunction, text: string, suspends: RegExp): boolean => {
+ if (suspends.test(text)) return false;
+ for (const callee of this.mentionedFunctions(text))
+ if (callee !== fn.name && parks.has(callee)) return false;
+ return true;
+ };
+ this.mod.functions.forEach((fn, index) => {
+ if (fn.async !== true || fn.generator !== undefined) return;
+ if (fn.asyncCacheGlobal !== undefined || fn.asyncCycleCacheGlobal !== undefined) return;
+ if (fn.returnType.kind === "dyn" || !parks.has(fn.name)) return;
+ const fiberText = fnDefs[index]!;
+ if (!qualifies(fn, fiberText.replace(AWAIT_SUSPENSIONS, ""), SUSPENDS)) return;
+ this.stacklessAsync.add(fn.name);
+ const coroText = this.emitFunction(fn);
+ if (!qualifies(fn, coroText, COROUTINE_SUSPENDS)) {
+ this.stacklessAsync.delete(fn.name);
+ return;
+ }
+ fnDefs[index] = coroText;
+ this.fnDefText.set(fn.name, coroText);
+ });
}
/** The spawn wrapper of a fiberless async function: runs the body on the
@@ -3813,13 +3994,85 @@ export class LlEmitter {
* entry's result is the awaited value of f's own body type; direct calls
* never carry a lifted environment. */
awaitCallEligible(fnName: string): boolean {
- if (this.wasi || this.mod.workers === true) return false;
+ // A coroutine body suspends itself at the await; the entry would park
+ // the stack it runs on.
+ if (this.wasi || this.mod.workers === true || this.currentCoro !== null) return false;
const fn = this.fnByName.get(fnName);
if (fn === undefined || fn.async !== true || fn.generator !== undefined) return false;
if (fn.captures !== undefined || fn.returnType.kind === "dyn") return false;
return fn.asyncCacheGlobal === undefined && fn.asyncCycleCacheGlobal === undefined;
}
+ /** Whether a stackless coroutine body's `await f(...)` may run f's body in
+ * place (emitCoroInlineAwait): f never suspends, and its direct call is
+ * one an await-call entry could take. */
+ inlineAwaitEligible(fnName: string): boolean {
+ if (this.wasi || this.currentCoro?.kind !== "async") return false;
+ const fn = this.fnByName.get(fnName);
+ if (fn === undefined || !this.inlineAwaitable.has(fnName)) return false;
+ return fn.captures === undefined;
+ }
+
+ /** The inline await entry of a fiberless async function (`sc_ai_*`): its
+ * body on the caller's stack between scr_async_inline_enter/exit, as in
+ * its spawn wrapper, returning the result and leaving a throw pending for
+ * the awaiting coroutine (emitCoroInlineAwait). */
+ private emitAsyncInlineAwaitCall(fn: IrFunction): string[] {
+ const retTy = this.llType(fn.returnType);
+ const params = fn.params.map((p, i) => `${this.llType(p.type)} %a${i}`);
+ const args = params.join(", ");
+ this.declare(`declare void @scr_async_inline_enter(ptr)`);
+ this.declare(`declare void @scr_async_inline_exit(ptr)`);
+ const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${args})`;
+ return [
+ `define internal ${retTy} @${mangleAsyncInlineAwaitCall(fn.name)}(${args}) ${FN_ATTRS} { ; inline await ${fn.name}`,
+ `entry:`,
+ ` %frame = alloca { ptr, ptr }`,
+ ` call void @scr_async_inline_enter(ptr %frame)`,
+ retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
+ ` call void @scr_async_inline_exit(ptr %frame)`,
+ retTy === "void" ? ` ret void` : ` ret ${retTy} %r`,
+ `}`,
+ ``,
+ ];
+ }
+
+ /** A stackless coroutine body's `await f(...)` of a fiberless f, the
+ * await-call entry's turns without its promise: the body runs in place,
+ * then a clean result takes the await's turn, and a throw becomes the
+ * rejected promise the await takes its turn on and re-throws, exactly as
+ * the spawn wrapper's promise would. An adopting await (`return f(...)`)
+ * first takes the adoption's then-job turn either way. */
+ emitCoroInlineAwait(call: ExprOf<"call">, adopt: boolean): LlValue {
+ const B = this.B;
+ const value = emitCallExpr(this, call, "inline");
+ const thrown = B.tmp();
+ for (const line of this.pendingTestLines(thrown)) B.line(line.trim());
+ if (adopt) this.emitCoroAwait(null);
+ const clean = B.newLabel("ai.clean");
+ const rejected = B.newLabel("ai.rejected");
+ const join = B.newLabel("ai.join");
+ B.condBr(thrown, rejected, clean);
+ B.startBlock(clean);
+ this.emitCoroAwait(null);
+ B.br(join);
+ B.startBlock(rejected);
+ this.declare(`declare ptr @scr_promise_new()`);
+ this.declare(`declare void @scr_promise_reject_pending(ptr)`);
+ this.declare(`declare void @scr_coro_await_void(ptr)`);
+ this.declare(`declare void @scr_promise_release(ptr)`);
+ const promise = B.tmp();
+ B.line(`${promise} = call ptr @scr_promise_new()`);
+ B.line(`call void @scr_promise_reject_pending(ptr ${promise})`);
+ this.emitCoroAwait(promise);
+ B.line(`call void @scr_coro_await_void(ptr ${promise})`);
+ B.line(`call void @scr_promise_release(ptr ${promise})`);
+ B.br(join);
+ B.startBlock(join);
+ this.emitPendingCheck();
+ return value;
+ }
+
/** The await-call entry of an async function (`sc_aw_*`): one direct
* `await f(...)` as a call returning the awaited value, so the call site
* needs no promise of its own. A fiberless body runs on the caller's
@@ -3884,6 +4137,21 @@ export class LlEmitter {
``,
];
}
+ if (argPackLines !== null && this.stacklessAsync.has(fn.name)) {
+ // A stackless body's eager prefix already runs on the awaiter's
+ // stack, which then awaits the promise like the two separate steps.
+ this.declare(`declare void @scr_promise_release(ptr)`);
+ return [
+ head,
+ `entry:`,
+ ` %p = call ptr @${mangleAsyncSpawn(fn.name)}(${args})`,
+ awaitLine("%v", "%p"),
+ ` call void @scr_promise_release(ptr %p)`,
+ retLine("%v"),
+ `}`,
+ ``,
+ ];
+ }
if (argPackLines !== null) {
const kind =
ret.kind === "void"
@@ -3984,6 +4252,9 @@ export class LlEmitter {
if (fiberless.has(fn.name) && this.adoptCallees.has(fn.name)) {
for (const line of this.emitAsyncAwaitCall(fn, null, true)) out.push(line);
}
+ if (this.inlineAwaitCallees.has(fn.name)) {
+ for (const line of this.emitAsyncInlineAwaitCall(fn)) out.push(line);
+ }
if (fiberless.has(fn.name)) {
const fieldTys = [
...(fn.captures !== undefined ? ["ptr"] : []),
@@ -4002,10 +4273,12 @@ export class LlEmitter {
if (this.adoptCallees.has(fn.name)) {
for (const line of this.emitAsyncAwaitCall(fn, argPackLines, true)) out.push(line);
}
+ const stackless = this.stacklessAsync.has(fn.name);
+ const spawn = stackless ? "scr_async_spawn_stackless" : "scr_async_spawn";
this.declare(`declare ptr @scr_fiber_promise(ptr)`);
- this.declare(`declare ptr @scr_async_spawn(ptr, ptr)`);
+ this.declare(`declare ptr @${spawn}(ptr, ptr)`);
this.needOom();
- if (this.wasi) {
+ if (this.wasi || stackless) {
// The coroutine body settles its own promise and owns the retained
// lifted environment until final suspension. Its initial call
// returns here at the first suspend (or final suspend).
@@ -4061,7 +4334,7 @@ export class LlEmitter {
...argPackLines,
];
sp.push(
- ` %p = call ptr @scr_async_spawn(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`,
+ ` %p = call ptr @${spawn}(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`,
...(cache !== null
? [
// The module loader owns this evaluation promise
@@ -4105,10 +4378,12 @@ export class LlEmitter {
}
/** Whether `fn`'s body is emitted as an LLVM switched coroutine: every
- * wasm32 async or generator body, and native synchronous generators. */
+ * wasm32 async or generator body, native synchronous generators, and the
+ * native async bodies selectStacklessAsync chose. */
private coroLowered(fn: IrFunction): boolean {
if (this.wasi) return fn.async === true || fn.generator !== undefined;
- return this.stacklessGenerators && fn.generator !== undefined && fn.async !== true;
+ if (fn.async === true) return fn.generator === undefined && this.stacklessAsync.has(fn.name);
+ return this.stacklessGenerators && fn.generator !== undefined;
}
/** Per-generator-function machinery — the async scaffolding's lazy
@@ -6129,6 +6404,53 @@ export class LlEmitter {
this.emitCoroSuspendPrepared();
}
+ /** Awaits in the current body suspend its coroutine: every wasm32 async
+ * body, and native stackless async bodies (selectStacklessAsync). */
+ get coroAwait(): boolean {
+ return this.wasi || this.currentCoro?.kind === "async";
+ }
+
+ /** Suspends the current coroutine body at an await of `promise` (null: a
+ * single microtask hop, as for a non-promise operand). A native body
+ * suspends unless the runtime continues it in place, where a fiber's
+ * hop would continue without switching (scr_coro_await_prepare). */
+ emitCoroAwait(promise: string | null): void {
+ if (this.wasi) {
+ this.emitWasiSuspend(promise);
+ return;
+ }
+ const coro = this.currentCoro;
+ if (coro === null)
+ throw new InternalCompilerError("llvm emitter bug: await suspension outside a coroutine");
+ const B = this.B;
+ const suspend = B.tmp();
+ if (promise === null) {
+ this.declare(`declare zeroext i1 @scr_coro_hop_prepare(ptr)`);
+ B.line(`${suspend} = call zeroext i1 @scr_coro_hop_prepare(ptr ${coro.self})`);
+ } else {
+ this.declare(`declare zeroext i1 @scr_coro_await_prepare(ptr, ptr)`);
+ B.line(
+ `${suspend} = call zeroext i1 @scr_coro_await_prepare(ptr ${coro.self}, ptr ${promise})`,
+ );
+ }
+ const park = B.newLabel("await.suspend");
+ const ready = B.newLabel("await.ready");
+ B.condBr(suspend, park, ready);
+ B.startBlock(park);
+ this.emitCoroSuspendPrepared();
+ B.br(ready);
+ B.startBlock(ready);
+ }
+
+ /** The runtime entry extracting an await's result of `kind`. A native
+ * coroutine has already suspended until the promise settled, so its
+ * extraction takes no further turn. */
+ awaitResultFn(kind: "void" | "f64" | "bool" | "str" | "ref"): string {
+ return !this.wasi && this.currentCoro?.kind === "async"
+ ? `scr_coro_await_${kind}`
+ : `scr_await_${kind}`;
+ }
+
/** A native generator's OUT slot (scr_async.c's ScrGen.out, mirrored by
* %ScrGenHead and %ScrGenSlot { kind, f64, b, payload, release_fn }). */
genOutSlot(gen: string): string {
@@ -6405,7 +6727,9 @@ export class LlEmitter {
this.localUnionStorageProofs = findLocalUnionStorage(fn, this.callLifetimes, this.unionSources);
this.localUnionStorage = new Map();
this.walkBorrows = this.walkBorrowsByFunction.get(fn.name) ?? new Set();
- this.integerRanges = analyzeIntegerRanges(numericFn, this.int32Slots.facts(fn.name));
+ const slotFacts = this.int32Slots.facts(fn.name);
+ this.integerRanges = analyzeIntegerRanges(numericFn, slotFacts);
+ this.int32Elements = slotFacts.elements ?? null;
this.bytesBounds = findBytesBounds(numericFn, this.integerRanges);
// Debug builds keep every number local a double for the debugger's
// variable view. Lazily boxed captured parameters keep their double,
@@ -6449,6 +6773,9 @@ export class LlEmitter {
this.currentGenerator = fn.generator ?? null;
this.currentAsyncGenerator = fn.async === true && fn.generator !== undefined;
this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
+ this.lastUseMoves.clear();
+ this.varRefCounts = null;
+ this.currentFunctionBody = fn.body;
this.inConstructor = this.constructorFunctions.has(fn.name);
this.currentCoro = null;
this.logArgSlots = 0;
@@ -6648,7 +6975,10 @@ export class LlEmitter {
if (this.storeInt32Local(p.localId, incoming)) continue;
B.line(`store ${this.llType(p.type)} %p_${mangleLocal(p.localId)}, ptr ${slot}`);
this.storeIntegerView(p.localId, incoming);
- if (isRefCounted(p.type) && !borrowed?.has(index)) fnScope.push({ slot, type: p.type });
+ if (isRefCounted(p.type) && !borrowed?.has(index)) {
+ fnScope.push({ slot, type: p.type });
+ this.noteParameterMove(fn, p.localId, slot);
+ }
if (this.reboundByFunction.get(fn.name)?.has(p.localId) && borrowed?.has(index)) {
// The caller's borrow stays in the parameter slot; values the body
// assigns are owned here until rebinding or exit releases them.
@@ -6707,8 +7037,9 @@ export class LlEmitter {
this.declare(`declare void @${finish}(ptr)`);
B.line(`call void @${finish}(ptr ${coro.self})`);
} else {
- this.declare(`declare void @scr_wasi_async_finish(ptr)`);
- B.line(`call void @scr_wasi_async_finish(ptr ${coro.self})`);
+ const finish = this.wasi ? "scr_wasi_async_finish" : "scr_coro_async_finish";
+ this.declare(`declare void @${finish}(ptr)`);
+ B.line(`call void @${finish}(ptr ${coro.self})`);
}
const finalState = B.tmp();
B.line(`${finalState} = call i8 @llvm.coro.suspend(token none, i1 true)`);
@@ -7216,6 +7547,91 @@ export class LlEmitter {
this.B.line(`call void @scr_async_gen_hop_done()`);
}
+ /** Owned-binding reads that take over the binding's reference, by read
+ * node (the binding's slot). */
+ private readonly lastUseMoves = new Map();
+ /** How often each varRef node object occurs in the current function:
+ * lowering may share one node between several reads, and a move keyed by
+ * node identity must fire for exactly one of them. */
+ private varRefCounts: Map | null = null;
+ private currentFunctionBody: IrStmt[] = [];
+
+ private varRefCount(e: IrExpr): number {
+ if (this.varRefCounts === null) {
+ const counts = new Map();
+ everyStmtList(this.currentFunctionBody, {
+ stmt: () => true,
+ expr: (node) => {
+ if (node.kind === "varRef") counts.set(node, (counts.get(node) ?? 0) + 1);
+ return true;
+ },
+ });
+ this.varRefCounts = counts;
+ }
+ return this.varRefCounts.get(e) ?? 0;
+ }
+
+ /** Moves need plain slots that nothing else reads behind the binding's
+ * back: no debugger view, no coroutine frame, no borrowed (+0) return. */
+ private lastUseMovesAllowed(): boolean {
+ return (
+ this.debug === null &&
+ !this.currentBorrowedReturn &&
+ this.currentCoro === null &&
+ this.currentGenerator === null
+ );
+ }
+
+ /** An owned local declared at the statement cursor whose last statement
+ * reads it once (lastUseRead): that read moves the reference. */
+ private noteLastUseMove(
+ s: IrStmt & { kind: "varDecl" },
+ b: ReturnType,
+ ): void {
+ const cursor = this.stmtCursor;
+ if (
+ !this.lastUseMovesAllowed() ||
+ cursor === null ||
+ cursor.stmts[cursor.index] !== s ||
+ b.kind !== "local" ||
+ b.local === undefined ||
+ b.local.boxed ||
+ b.local.tdz ||
+ this.integerViews.has(s.localId)
+ )
+ return;
+ const read = lastUseRead(cursor.stmts, cursor.index, s.localId);
+ if (read !== null && this.varRefCount(read) === 1) this.lastUseMoves.set(read, b.slot);
+ }
+
+ /** An owned parameter whose last statement in the body reads it once:
+ * that read moves the reference. Walk locals may borrow from an owned
+ * parameter's slot (they need it until the function returns), so bodies
+ * with walks keep their parameters, and locals rebound from the
+ * parameter count as derived from it. */
+ private noteParameterMove(fn: IrFunction, localId: string, slot: string): void {
+ if (
+ !this.lastUseMovesAllowed() ||
+ fn.async ||
+ fn.generator !== undefined ||
+ (this.walkBorrowsByFunction.get(fn.name)?.size ?? 0) > 0 ||
+ this.reboundByFunction.get(fn.name)?.has(localId) === true ||
+ this.integerViews.has(localId)
+ )
+ return;
+ const read = lastUseRead(fn.body, -1, localId, true);
+ if (read !== null && this.varRefCount(read) === 1) this.lastUseMoves.set(read, slot);
+ }
+
+ /** Emits the move of a noted last read: the slot is cleared, so scope
+ * exits release nothing and the read's owned temp carries the reference. */
+ takeLastUseMove(e: IrExpr, slot: string, type: IrType): boolean {
+ if (this.lastUseMoves.get(e) !== slot || !typeEquals(type, e.type)) return false;
+ this.lastUseMoves.delete(e);
+ this.B.line(`store ptr null, ptr ${slot}`);
+ return true;
+ }
+
/** `return x` of a local whose slot is an owned scope entry of the same
* type: the return can take over the local's reference. */
private movableReturn(
@@ -7584,6 +8000,7 @@ export class LlEmitter {
this.storeIntegerView(s.localId, v, s.init);
if (isRefCounted(b.type)) {
this.scopes[this.scopes.length - 1]!.push({ slot: b.slot, type: b.type });
+ this.noteLastUseMove(s, b);
}
break;
}
@@ -7736,6 +8153,28 @@ export class LlEmitter {
B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
B.condBr(same, done, resize);
B.startBlock(resize);
+ if (s.pop === true) {
+ // A discarded pop's `length - 1` on a packed array removes the
+ // last element inline, then releases it.
+ const call = B.newLabel("arr.len.call"),
+ pop = B.newLabel("arr.len.pop");
+ const lastNumber = B.tmp(),
+ dropsLast = B.tmp();
+ B.line(`${lastNumber} = fsub double ${lenNumber}, 1.0`);
+ B.line(`${dropsLast} = fcmp oeq double ${length.name}, ${lastNumber}`);
+ B.condBr(dropsLast, pop, call);
+ B.startBlock(pop);
+ const elem = s.arr.type.elem;
+ const acc = elemAccess(elem);
+ const raw = emitDenseArrayPop(this, arr.name, acc, call, call);
+ if (acc === "ref" && isRefCounted(elem)) {
+ const old = B.tmp();
+ B.line(`${old} = inttoptr i64 ${raw} to ptr`);
+ this.releaseValue(old, elem);
+ }
+ B.br(done);
+ B.startBlock(call);
+ }
B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`);
this.emitPendingCheck();
B.br(done);
@@ -8587,6 +9026,12 @@ export class LlEmitter {
// own turn is the await (nothing runs in between).
const genSettles =
this.currentAsyncGenerator && (s.awaited === true || this.finallyStack.length > 0);
+ // A completion turn that awaits x replaces a return() request's
+ // already-taken await.
+ if (this.currentAsyncGenerator && !genSettles) {
+ this.declare(`declare void @scr_async_gen_return_awaits()`);
+ B.line(`call void @scr_async_gen_return_awaits()`);
+ }
if (this.currentAsyncGenerator && s.awaited !== true && this.finallyStack.length > 0) {
if (this.wasi) this.emitWasiSuspend(null);
else {
diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts
index 0075cedea..6731e13c9 100644
--- a/packages/compiler/src/backend/llvm/expr-async.ts
+++ b/packages/compiler/src/backend/llvm/expr-async.ts
@@ -338,8 +338,17 @@ export function emitSerializationExpr(
// checked source can throw from the dyn walker: finish still runs
// (frees the buffer, the partial string joins the frame and
// releases on unwind), then the pending check unwinds — the
- // runtime ABI’s contract exactly.
- if (host.walkers.jsonWriteMayThrow(e.value.type)) host.emitPendingCheck();
+ // runtime ABI’s contract exactly. Under %j the runtime first turns
+ // a circular-structure TypeError into the text "[Circular]".
+ if (host.walkers.jsonWriteMayThrow(e.value.type)) {
+ if (e.circular) {
+ host.declare(`declare ptr @scr_util_format_j_text(ptr)`);
+ const shown = B.tmp();
+ B.line(`${shown} = call ptr @scr_util_format_j_text(ptr ${t})`);
+ compact = host.own({ name: shown, type: e.type });
+ }
+ host.emitPendingCheck();
+ }
}
// A pretty-print form (`stringify(v, null, 2)`): the frontend
// resolved the space to a compile-time indent string (Node's
@@ -892,6 +901,17 @@ export function emitAsyncExpr(
// the callee's adopting entry. Past finally regions, the return has
// already produced the promise (adoptedOperand).
const adopted = e.adopt === true ? host.takeAdoptedOperand() : null;
+ // A stackless coroutine body runs a never-suspending callee in place
+ // and takes the await's turn itself.
+ if (adopted === null && e.value.kind === "call" && host.inlineAwaitEligible(e.value.callee)) {
+ const callee = host.fnByName.get(e.value.callee)!;
+ if (
+ typeEquals(callee.returnType, e.type) &&
+ !host.callLifetimes.borrowed.has(e.value.callee)
+ ) {
+ return host.emitCoroInlineAwait(e.value, e.adopt === true);
+ }
+ }
if (adopted === null && e.value.kind === "call" && host.awaitCallEligible(e.value.callee)) {
const callee = host.fnByName.get(e.value.callee)!;
if (
@@ -903,32 +923,37 @@ export function emitAsyncExpr(
}
const pr = adopted ?? host.emitExpr(e.value);
if (e.adopt === true) {
- if (host.wasi) host.emitWasiSuspend(null);
+ if (host.coroAwait) host.emitCoroAwait(null);
else {
host.declare(`declare void @scr_await_hop()`);
B.line(`call void @scr_await_hop()`);
}
}
- if (host.wasi) host.emitWasiSuspend(pr.name);
+ if (host.coroAwait) host.emitCoroAwait(pr.name);
if (e.type.kind === "void") {
- host.declare(`declare void @scr_await_void(ptr)`);
- B.line(`call void @scr_await_void(ptr ${pr.name})`);
+ const voidAwait = host.awaitResultFn("void");
+ host.declare(`declare void @${voidAwait}(ptr)`);
+ B.line(`call void @${voidAwait}(ptr ${pr.name})`);
host.emitPendingCheck();
return { name: "", type: e.type };
}
const t = B.tmp();
if (e.type.kind === "f64" || e.type.kind === "date") {
- host.declare(`declare double @scr_await_f64(ptr)`);
- B.line(`${t} = call double @scr_await_f64(ptr ${pr.name})`);
+ const f64Await = host.awaitResultFn("f64");
+ host.declare(`declare double @${f64Await}(ptr)`);
+ B.line(`${t} = call double @${f64Await}(ptr ${pr.name})`);
} else if (e.type.kind === "bool") {
- host.declare(`declare zeroext i1 @scr_await_bool(ptr)`);
- B.line(`${t} = call zeroext i1 @scr_await_bool(ptr ${pr.name})`);
+ const boolAwait = host.awaitResultFn("bool");
+ host.declare(`declare zeroext i1 @${boolAwait}(ptr)`);
+ B.line(`${t} = call zeroext i1 @${boolAwait}(ptr ${pr.name})`);
} else if (e.type.kind === "string") {
- host.declare(`declare ptr @scr_await_str(ptr)`);
- B.line(`${t} = call ptr @scr_await_str(ptr ${pr.name})`);
+ const strAwait = host.awaitResultFn("str");
+ host.declare(`declare ptr @${strAwait}(ptr)`);
+ B.line(`${t} = call ptr @${strAwait}(ptr ${pr.name})`);
} else {
- host.declare(`declare ptr @scr_await_ref(ptr)`);
- B.line(`${t} = call ptr @scr_await_ref(ptr ${pr.name})`);
+ const refAwait = host.awaitResultFn("ref");
+ host.declare(`declare ptr @${refAwait}(ptr)`);
+ B.line(`${t} = call ptr @${refAwait}(ptr ${pr.name})`);
}
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
@@ -960,15 +985,16 @@ export function emitAsyncExpr(
const lj = B.newLabel("au.j");
B.condBr(isP, lp, lh);
B.startBlock(lp);
- host.declare(`declare void @scr_await_void(ptr)`);
+ const voidAwait = host.awaitResultFn("void");
+ host.declare(`declare void @${voidAwait}(ptr)`);
{
const promise = host.unionPeek(u.name, def.id);
- if (host.wasi) host.emitWasiSuspend(promise);
- B.line(`call void @scr_await_void(ptr ${promise})`);
+ if (host.coroAwait) host.emitCoroAwait(promise);
+ B.line(`call void @${voidAwait}(ptr ${promise})`);
}
B.br(lj);
B.startBlock(lh);
- if (host.wasi) host.emitWasiSuspend(null);
+ if (host.coroAwait) host.emitCoroAwait(null);
else B.line(`call void @scr_await_hop()`);
B.br(lj);
B.startBlock(lj);
@@ -1000,33 +1026,37 @@ export function emitAsyncExpr(
B.condBr(isP, lp, lh);
B.startBlock(lp);
const peek = host.unionPeek(u.name, def.id);
- if (host.wasi) host.emitWasiSuspend(peek);
+ if (host.coroAwait) host.emitCoroAwait(peek);
let awaited: LlValue;
if (inner.kind === "f64" || inner.kind === "date") {
- host.declare(`declare double @scr_await_f64(ptr)`);
+ const f64Await = host.awaitResultFn("f64");
+ host.declare(`declare double @${f64Await}(ptr)`);
const x = B.tmp();
- B.line(`${x} = call double @scr_await_f64(ptr ${peek})`);
+ B.line(`${x} = call double @${f64Await}(ptr ${peek})`);
awaited = { name: x, type: inner };
} else if (inner.kind === "bool") {
- host.declare(`declare zeroext i1 @scr_await_bool(ptr)`);
+ const boolAwait = host.awaitResultFn("bool");
+ host.declare(`declare zeroext i1 @${boolAwait}(ptr)`);
const x = B.tmp();
- B.line(`${x} = call zeroext i1 @scr_await_bool(ptr ${peek})`);
+ B.line(`${x} = call zeroext i1 @${boolAwait}(ptr ${peek})`);
awaited = { name: x, type: inner };
} else if (inner.kind === "string") {
- host.declare(`declare ptr @scr_await_str(ptr)`);
+ const strAwait = host.awaitResultFn("str");
+ host.declare(`declare ptr @${strAwait}(ptr)`);
const x = B.tmp();
- B.line(`${x} = call ptr @scr_await_str(ptr ${peek})`);
+ B.line(`${x} = call ptr @${strAwait}(ptr ${peek})`);
awaited = { name: x, type: inner };
} else {
- host.declare(`declare ptr @scr_await_ref(ptr)`);
+ const refAwait = host.awaitResultFn("ref");
+ host.declare(`declare ptr @${refAwait}(ptr)`);
const x = B.tmp();
- B.line(`${x} = call ptr @scr_await_ref(ptr ${peek})`);
+ B.line(`${x} = call ptr @${refAwait}(ptr ${peek})`);
awaited = { name: x, type: inner };
}
B.line(`store ptr ${host.unionNewOwned(resUnionId, innerTag, awaited)}, ptr ${slot}`);
B.br(lj);
B.startBlock(lh);
- if (host.wasi) host.emitWasiSuspend(null);
+ if (host.coroAwait) host.emitCoroAwait(null);
else B.line(`call void @scr_await_hop()`);
const unitTags = def.arms.flatMap((a, i) => (isUnitType(a) ? [i] : []));
if (unitTags.length === 1) {
diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts
index 33c24fd54..ac025df09 100644
--- a/packages/compiler/src/backend/llvm/expr-bytes.ts
+++ b/packages/compiler/src/backend/llvm/expr-bytes.ts
@@ -7,6 +7,7 @@ import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { emitByteNumber } from "./byte-numbers.js";
+import { integerOrNaNRange } from "./wrapping-sums.js";
const BYTES_NUM_KIND: Record = {
u8: { kind: 0, le: false },
@@ -43,6 +44,7 @@ const DV_GET_KIND: Record = {
dvGetFloat64: 7,
dvGetBigUint64Number: 8,
dvGetBigInt64Number: 9,
+ dvGetFloat16: 10,
};
const DV_SET_KIND: Record = {
@@ -54,6 +56,7 @@ const DV_SET_KIND: Record = {
dvSetInt32: 5,
dvSetFloat32: 6,
dvSetFloat64: 7,
+ dvSetFloat16: 10,
};
export function emitIntegerLoopIndex(host: LlvmEmitterContext, expr: IrExpr): string | null {
@@ -272,7 +275,7 @@ function emitSharedOrLocalLoad(
idx: string,
): LlValue {
const B = host.B;
- const floating = elem === "f32" || elem === "f64";
+ const floating = elem === "f16" || elem === "f32" || elem === "f64";
const fn = floating ? "scr_bytes_shared_get_f64" : "scr_bytes_shared_get_int";
const ret = floating ? "double" : "i32";
host.declare(`declare ${ret} @${fn}(ptr, ${host.sizeType})`);
@@ -313,6 +316,16 @@ function emitBytesLoadAt(
const B = host.B;
const data = host.emitBytesData(receiver);
const p = B.tmp();
+ if (elem === "f16") {
+ // Half-precision bits widen exactly; the runtime converts them.
+ host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`);
+ const raw = B.tmp();
+ const out = B.tmp();
+ B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
+ B.line(`${raw} = load i16, ptr ${p}, align 1`);
+ B.line(`${out} = call double @scr_f16_to_f64(i16 zeroext ${raw})`);
+ return { name: out, type: F64 };
+ }
if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const signed = elem === "i8" || elem === "i16";
@@ -362,6 +375,20 @@ export function emitToUint32(
B.line(`${out} = trunc i64 ${integer} to i32`);
return out;
}
+ if (expr && integerOrNaNRange(host, expr)) {
+ // An exact integer or NaN (an Int32[] or typed-array element read that
+ // may miss, `charCodeAt`): the integer's low 32 bits, or 0 for NaN. The
+ // conversion of NaN is poison, but the select then picks 0.
+ const integer = B.tmp();
+ const low = B.tmp();
+ const ordered = B.tmp();
+ const out = B.tmp();
+ B.line(`${integer} = fptosi double ${value} to i64`);
+ B.line(`${low} = trunc i64 ${integer} to i32`);
+ B.line(`${ordered} = fcmp ord double ${value}, 0.0`);
+ B.line(`${out} = select i1 ${ordered}, i32 ${low}, i32 0`);
+ return out;
+ }
const aboveMin = B.tmp();
const belowMax = B.tmp();
const fast = B.tmp();
@@ -457,10 +484,19 @@ export function emitBytesSet(
B.line(`${stored} = fptoui double ${clamped} to i32`);
} else
stored =
- elem === "f32" || elem === "f64" ? null : host.emitToUint32(value.name, expr, value.uint32);
+ elem === "f16" || elem === "f32" || elem === "f64"
+ ? null
+ : host.emitToUint32(value.name, expr, value.uint32);
const data = host.emitBytesData(receiver);
const p = B.tmp();
- if (BYTES_ELEMENT_SIZE[elem] < 4) {
+ if (elem === "f16") {
+ // One round-to-nearest-even step from the double, in the runtime.
+ host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`);
+ const narrowed = B.tmp();
+ B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`);
+ B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
+ B.line(`store i16 ${narrowed}, ptr ${p}, align 1`);
+ } else if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const byte = B.tmp();
B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
@@ -867,6 +903,7 @@ export function emitBytesIntrinsic(
case "dvGetInt16":
case "dvGetUint32":
case "dvGetInt32":
+ case "dvGetFloat16":
case "dvGetFloat32":
case "dvGetFloat64":
case "dvGetBigUint64Number":
@@ -897,6 +934,7 @@ export function emitBytesIntrinsic(
case "dvSetInt16":
case "dvSetUint32":
case "dvSetInt32":
+ case "dvSetFloat16":
case "dvSetFloat32":
case "dvSetFloat64": {
// DataView setters: [offset, value, littleEndian?] — void; throw
diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts
index 8c748cc93..6e8b8caf4 100644
--- a/packages/compiler/src/backend/llvm/expr-calls.ts
+++ b/packages/compiler/src/backend/llvm/expr-calls.ts
@@ -20,6 +20,7 @@ import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js"
import {
mangleAsyncAdoptCall,
mangleAsyncAwaitCall,
+ mangleAsyncInlineAwaitCall,
mangleBorrowedFunction,
mangleBorrowingWrapper,
mangleClassNew,
@@ -70,7 +71,7 @@ export function emitCallExpr(
| "instanceOf"
| "virtualCall"
>,
- awaitEntry: false | "await" | "adopt" = false,
+ awaitEntry: false | "await" | "adopt" | "inline" = false,
/** A projection consumes a scalar union result (Int32Abi.union): answer
* a private stack box instead of a heap box. */
stackResult = false,
@@ -171,6 +172,21 @@ export function emitCallExpr(
: `${host.llType(callee.params[i]!.type)} ${a.name}`,
)
.join(", ");
+ if (awaitEntry === "inline") {
+ // A stackless coroutine's await of a fiberless callee: the entry
+ // runs the body in place and leaves a throw pending; the enclosing
+ // await takes the turn and tests (emitCoroInlineAwait).
+ host.inlineAwaitCallees.add(e.callee);
+ const awaited = callee.returnType;
+ const entry = `@${mangleAsyncInlineAwaitCall(e.callee)}`;
+ if (awaited.kind === "void") {
+ B.line(`call void ${entry}(${argList})`);
+ return { name: "", type: awaited };
+ }
+ const t = B.tmp();
+ B.line(`${t} = call ${host.llType(awaited)} ${entry}(${argList})`);
+ return host.own({ name: t, type: awaited });
+ }
if (awaitEntry) {
// The enclosing await routes this call through the callee's
// await-call entry: the result is the awaited value, and the
@@ -317,6 +333,16 @@ export function emitCallExpr(
natArgs.push(`ptr ${data}`, `${host.sizeType} ${len}`);
break;
}
+ case "cstring": {
+ // The string's own NUL-terminated data, after the check that
+ // it holds no U+0000 (a C string cannot carry one).
+ host.declare(`declare ptr @scr_library_cstr_arg(ptr)`);
+ const data = B.tmp();
+ B.line(`${data} = call ptr @scr_library_cstr_arg(ptr ${arg.name})`);
+ natTypes.push("ptr");
+ natArgs.push(`ptr ${data}`);
+ break;
+ }
case "bytes": {
const lenPtr = B.tmp();
const len = B.tmp();
diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts
index a2d5fd35a..9c77690f8 100644
--- a/packages/compiler/src/backend/llvm/expr-containers.ts
+++ b/packages/compiler/src/backend/llvm/expr-containers.ts
@@ -49,7 +49,11 @@ import {
STRING_UTF16_LEN_DECL,
} from "./string-reads.js";
import { exactInteger, widenInteger } from "./integer-values.js";
-import { emitDenseArrayNumber, emitDenseArrayPush } from "./dense-array-access.js";
+import {
+ emitDenseArrayNumber,
+ emitDenseArrayPop,
+ emitDenseArrayPush,
+} from "./dense-array-access.js";
import { emitUnionWiden } from "./expr-records.js";
export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string {
@@ -667,19 +671,38 @@ export function emitArrIntrinsic(
const rawSlot = B.slot();
const resultSlot = B.slot();
B.entryAllocas.push(`${rawSlot} = alloca i64`, `${resultSlot} = alloca ptr`);
- B.line(`store i64 0, ptr ${rawSlot}`);
- const state = B.tmp();
- const has = B.tmp();
- B.line(`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})`);
- B.line(`${has} = icmp eq i8 ${state}, 1`);
const lp = B.newLabel("remove.p");
const la = B.newLabel("remove.a");
const lj = B.newLabel("remove.j");
+ // pop answers an empty or packed array inline; shift and every other
+ // pop take the runtime call.
+ let denseRaw: string | null = null;
+ let denseLabel = "";
+ let slowLabel = "";
+ if (method === "pop") {
+ slowLabel = B.newLabel("remove.slow");
+ denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel);
+ denseLabel = B.currentLabel();
+ B.br(lp);
+ B.startBlock(slowLabel);
+ }
+ B.line(`store i64 0, ptr ${rawSlot}`);
+ const state = B.tmp();
+ const has = B.tmp();
+ const slowRaw = B.tmp();
+ B.line(
+ `${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})${denseRaw === null ? "" : " cold"}`,
+ );
+ B.line(`${slowRaw} = load i64, ptr ${rawSlot}`);
+ B.line(`${has} = icmp eq i8 ${state}, 1`);
B.condBr(has, lp, la);
B.startBlock(lp);
- const raw = B.tmp();
+ let raw = slowRaw;
const value = B.tmp();
- B.line(`${raw} = load i64, ptr ${rawSlot}`);
+ if (denseRaw !== null) {
+ raw = B.tmp();
+ B.line(`${raw} = phi i64 [ ${denseRaw}, %${denseLabel} ], [ ${slowRaw}, %${slowLabel} ]`);
+ }
if (into) {
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
let bits = raw;
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts
index 016ae0e7c..aaae4c35c 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.ts
@@ -120,6 +120,8 @@ export function emitLiteralExpr(
// An immortal Immutable constant: nothing to count.
if (b.kind === "global" && host.immortalGlobals.has(e.localId))
return host.markImmortal({ name: t, type: e.type });
+ // The binding's last read takes over its reference (lastUseRead).
+ if (host.takeLastUseMove(e, b.slot, b.type)) return host.own({ name: t, type: e.type });
return host.own({ name: host.retainValue(t, e.type), type: e.type });
}
return { name: t, type: e.type };
@@ -309,7 +311,11 @@ export function emitOperatorExpr(
}
if (e.op === "%") {
const constant = constantDivisor(e.right, ri);
- const power = constant === null ? null : emitPowerOfTwoRemainder(host, l, constant);
+ // Sums, differences and products are usually integers.
+ const computed =
+ e.left.kind === "bin" && (e.left.op === "+" || e.left.op === "-" || e.left.op === "*");
+ const power =
+ constant === null ? null : emitPowerOfTwoRemainder(host, l, constant, li, computed);
if (power) return power;
// A fractional constant divisor never passes the integer check.
const checked =
diff --git a/packages/compiler/src/backend/llvm/integer-remainder.ts b/packages/compiler/src/backend/llvm/integer-remainder.ts
index 144695a45..a877bc625 100644
--- a/packages/compiler/src/backend/llvm/integer-remainder.ts
+++ b/packages/compiler/src/backend/llvm/integer-remainder.ts
@@ -188,43 +188,204 @@ function isPowerOfTwo(value: number): boolean {
return power === value;
}
+/** `x - trunc(x / c) * c` for a power of two c of at least 1, which is
+ * exact for every double x. When |x| < c the quotient's magnitude is below
+ * 1 and truncates to zero, so the result is x. Otherwise x / c is a normal
+ * double and exact, and the truncated quotient times c is a multiple of c
+ * of magnitude at most |x|, also exact. When c is below x's ulp, x is
+ * itself a multiple of c and the difference is zero; otherwise both terms
+ * are multiples of x's ulp and the difference, smaller than c, is exact.
+ * ±Infinity gives the default NaN of `Infinity - Infinity`, the NaN fmod
+ * also returns for it, and a NaN dividend propagates. A zero difference
+ * takes the dividend's sign (`-4 % 2` and `-0 % 1` are -0). */
+function emitPowerOfTwoDouble(host: LlvmEmitterContext, left: string, magnitude: number): string {
+ const B = host.B;
+ const c = f64Lit(magnitude);
+ const quotient = B.tmp();
+ B.line(`${quotient} = fdiv double ${left}, ${c}`);
+ const whole = emitTruncF64(host, quotient);
+ const multiple = B.tmp(),
+ difference = B.tmp();
+ B.line(`${multiple} = fmul double ${whole}, ${c}`);
+ B.line(`${difference} = fsub double ${left}, ${multiple}`);
+ return signDividendZero(host, left, difference);
+}
+
+/** `value`, or a zero with the dividend's sign when `value` is zero. */
+function signDividendZero(host: LlvmEmitterContext, left: string, value: string): string {
+ const B = host.B;
+ const zero = B.tmp(),
+ signedZero = B.tmp(),
+ result = B.tmp();
+ host.declare("declare double @llvm.copysign.f64(double, double)");
+ B.line(`${zero} = fcmp oeq double ${value}, ${f64Lit(0)}`);
+ B.line(`${signedZero} = call double @llvm.copysign.f64(double ${f64Lit(0)}, double ${left})`);
+ B.line(`${result} = select i1 ${zero}, double ${signedZero}, double ${value}`);
+ return result;
+}
+
+/** 2^52: the largest power-of-two divisor the integer path takes. */
+const INTEGER_DIVISOR_LIMIT = 2 ** 52;
+
+/** 2^62 as a double: the bound on dividends the integer path accepts. */
+const DOUBLE_DIVIDEND_LIMIT = 2 ** 62;
+
/** JS `%` by a constant divisor whose magnitude c is a power of two of at
- * least 1, as `x - trunc(x / c) * c`, which is exact for every double x.
- * When |x| < c the quotient's magnitude is below 1 and truncates to zero,
- * so the result is x. Otherwise x / c is a normal double and exact, and
- * the truncated quotient times c is a multiple of c of magnitude at most
- * |x|, also exact. When c is below x's ulp, x is itself a multiple of c and
- * the difference is zero; otherwise both terms are multiples of x's ulp and
- * the difference, smaller than c, is exact. ±Infinity gives the default
- * NaN of `Infinity - Infinity`, the NaN fmod also returns for it, and a NaN
- * dividend propagates. A zero difference takes the dividend's sign
- * (`-4 % 2` and `-0 % 1` are -0). The divisor's sign does not change the
- * result. Returns null for any other divisor; a power of two below 1 keeps
- * frem, because x / c can overflow. */
+ * least 1. The divisor's sign does not change the result. Returns null for
+ * any other divisor; a power of two below 1 keeps frem, because x / c can
+ * overflow.
+ *
+ * Where truncation is itself an integer round trip (x86-64 without SSE4.1,
+ * see trunc.ts), `x - trunc(x / c) * c` puts two multiplications, the round
+ * trip and a sign fix on the dividend's dependency chain. In a loop that
+ * feeds the result back, such as `s = (s + x) % 65536`, that costs more
+ * than the integer remainder. There a proven integer dividend takes the i64
+ * remainder directly, and other dividends of magnitude below 2^62 take
+ * integer paths:
+ *
+ * - A computed dividend (`+`, `-` or `*`, as in hashes, generators and
+ * accumulators) is usually an integer. When it is one, the result is the
+ * i64 remainder (shifts, since c is a constant power of two), as in
+ * emitCheckedRemainder. Otherwise, when |x| < 2c (as in a phase
+ * accumulator reduced by 1), the result is x itself if |x| < c and
+ * x - copysign(c, x) if not, exact because x and c are then within a
+ * factor of two, and never zero because x is not an integer. Larger
+ * quotients take the path below, whose cost does not grow with the
+ * quotient as fmod's does.
+ * - Any other dividend takes i = trunc(x) as an i64 and adds back x's
+ * fraction without a branch on whether x is an integer, which stays fast
+ * when integers and fractions alternate unpredictably. Because c is an
+ * integer, trunc(x / c) = trunc(i / c), so
+ * x % c = (i - trunc(i / c) * c) + (x - i) = (i srem c) + (x - i).
+ * Both terms carry x's sign or are zero; x - i is exact, and the sum, of
+ * magnitude below c and a multiple of x's ulp (an integer when x is one),
+ * is exact too. It is zero exactly when both terms are.
+ *
+ * A zero result takes the dividend's sign. Larger, infinite and NaN
+ * dividends take the double sequence, as do targets with a rounding
+ * instruction and divisors above 2^52. */
export function emitPowerOfTwoRemainder(
host: LlvmEmitterContext,
left: LlValue,
divisor: number,
+ dividend: LlInteger | null,
+ computed: boolean,
): LlValue | null {
const magnitude = Math.abs(divisor);
if (!isPowerOfTwo(magnitude)) return null;
+ if (!host.inlineTrunc || magnitude > INTEGER_DIVISOR_LIMIT)
+ return { name: emitPowerOfTwoDouble(host, left.name, magnitude), type: F64 };
+ if (dividend)
+ return emitSignedIntegerRemainder(host, left, dividend, {
+ name: String(magnitude),
+ type: "i64",
+ signed: false,
+ range: { min: magnitude, max: magnitude },
+ });
const B = host.B;
- const c = f64Lit(magnitude);
- const quotient = B.tmp();
- B.line(`${quotient} = fdiv double ${left.name}, ${c}`);
- const whole = emitTruncF64(host, quotient);
- const multiple = B.tmp(),
- difference = B.tmp(),
+ host.declare("declare double @llvm.fabs.f64(double)");
+ host.declare("declare double @llvm.copysign.f64(double, double)");
+ host.declare("declare i1 @llvm.expect.i1(i1, i1)");
+ const raw = B.tmp(),
+ integer = B.tmp(),
+ back = B.tmp(),
+ size = B.tmp(),
+ inRange = B.tmp();
+ B.line(`${raw} = fptosi double ${left.name} to i64`);
+ // Poison when out of range or NaN; no path below uses it then.
+ B.line(`${integer} = freeze i64 ${raw}`);
+ B.line(`${back} = sitofp i64 ${integer} to double`);
+ B.line(`${size} = call double @llvm.fabs.f64(double ${left.name})`);
+ // False for NaN.
+ B.line(`${inRange} = fcmp olt double ${size}, ${f64Lit(DOUBLE_DIVIDEND_LIMIT)}`);
+ const join = B.newLabel("rem.pow2.join");
+ const incoming: string[] = [];
+ const branch = (condition: string, likely: string, unlikely: string): void => {
+ const expected = B.tmp();
+ B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${condition}, i1 true)`);
+ B.condBr(expected, likely, unlikely);
+ };
+ if (computed) {
+ const exact = B.tmp(),
+ integral = B.tmp();
+ B.line(`${exact} = fcmp oeq double ${back}, ${left.name}`);
+ B.line(`${integral} = and i1 ${inRange}, ${exact}`);
+ const whole = B.newLabel("rem.pow2.int"),
+ other = B.newLabel("rem.pow2.other");
+ branch(integral, whole, other);
+ B.startBlock(whole);
+ const remainder = B.tmp(),
+ number = B.tmp(),
+ zero = B.tmp(),
+ signedZero = B.tmp(),
+ integerValue = B.tmp();
+ B.line(`${remainder} = srem i64 ${integer}, ${magnitude}`);
+ B.line(`${number} = sitofp i64 ${remainder} to double`);
+ B.line(`${zero} = icmp eq i64 ${remainder}, 0`);
+ B.line(`${signedZero} = fmul double ${left.name}, ${f64Lit(0)}`);
+ B.line(`${integerValue} = select i1 ${zero}, double ${signedZero}, double ${number}`);
+ incoming.push(`[ ${integerValue}, %${B.currentLabel()} ]`);
+ B.br(join);
+ B.startBlock(other);
+ const near = B.tmp();
+ // False for NaN.
+ B.line(`${near} = fcmp olt double ${size}, ${f64Lit(2 * magnitude)}`);
+ const small = B.newLabel("rem.pow2.small"),
+ large = B.newLabel("rem.pow2.large");
+ branch(near, small, large);
+ B.startBlock(small);
+ const c = f64Lit(magnitude);
+ const below = B.tmp();
+ B.line(`${below} = fcmp olt double ${size}, ${c}`);
+ // A branch rather than a select keeps the usual |x| < c case, where the
+ // result is x, off the dividend's dependency chain.
+ const keep = B.newLabel("rem.pow2.keep"),
+ wrap = B.newLabel("rem.pow2.wrap");
+ branch(below, keep, wrap);
+ B.startBlock(keep);
+ incoming.push(`[ ${left.name}, %${B.currentLabel()} ]`);
+ B.br(join);
+ B.startBlock(wrap);
+ const step = B.tmp(),
+ reduced = B.tmp();
+ B.line(`${step} = call double @llvm.copysign.f64(double ${c}, double ${left.name})`);
+ B.line(`${reduced} = fsub double ${left.name}, ${step}`);
+ incoming.push(`[ ${reduced}, %${B.currentLabel()} ]`);
+ B.br(join);
+ B.startBlock(large);
+ }
+ const fractional = B.newLabel("rem.pow2.frac"),
+ huge = B.newLabel("rem.pow2.float");
+ branch(inRange, fractional, huge);
+ B.startBlock(fractional);
+ const remainder = B.tmp(),
+ whole = B.tmp(),
+ fraction = B.tmp(),
+ sum = B.tmp(),
+ wholeZero = B.tmp(),
+ fractionZero = B.tmp(),
zero = B.tmp(),
signedZero = B.tmp(),
- result = B.tmp();
- host.declare("declare double @llvm.copysign.f64(double, double)");
- B.line(`${multiple} = fmul double ${whole}, ${c}`);
- B.line(`${difference} = fsub double ${left.name}, ${multiple}`);
- B.line(`${zero} = fcmp oeq double ${difference}, ${f64Lit(0)}`);
+ value = B.tmp();
+ B.line(`${remainder} = srem i64 ${integer}, ${magnitude}`);
+ B.line(`${whole} = sitofp i64 ${remainder} to double`);
+ B.line(`${fraction} = fsub double ${left.name}, ${back}`);
+ B.line(`${sum} = fadd double ${whole}, ${fraction}`);
+ B.line(`${wholeZero} = icmp eq i64 ${remainder}, 0`);
+ B.line(`${fractionZero} = fcmp oeq double ${fraction}, ${f64Lit(0)}`);
+ B.line(`${zero} = and i1 ${wholeZero}, ${fractionZero}`);
B.line(
`${signedZero} = call double @llvm.copysign.f64(double ${f64Lit(0)}, double ${left.name})`,
);
- B.line(`${result} = select i1 ${zero}, double ${signedZero}, double ${difference}`);
+ B.line(`${value} = select i1 ${zero}, double ${signedZero}, double ${sum}`);
+ incoming.push(`[ ${value}, %${B.currentLabel()} ]`);
+ B.br(join);
+ B.startBlock(huge);
+ const floating = emitPowerOfTwoDouble(host, left.name, magnitude);
+ incoming.push(`[ ${floating}, %${B.currentLabel()} ]`);
+ B.br(join);
+ B.startBlock(join);
+ const result = B.tmp();
+ B.line(`${result} = phi double ${incoming.join(", ")}`);
return { name: result, type: F64 };
}
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index 3ec42605b..4a9f4017a 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -11,7 +11,12 @@ import { InternalCompilerError } from "../../errors.js";
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
-import { ASSERT_VERDICT_LIB_FNS, LIB_FN_SYMS, USES_TIMERS_LIB_FNS } from "./lib-shared.js";
+import {
+ ASSERT_VERDICT_LIB_FNS,
+ LIB_FN_SYMS,
+ mockTimerSym,
+ USES_TIMERS_LIB_FNS,
+} from "./lib-shared.js";
import { isDynUndefined } from "../../ir/runtime-features.js";
import { DYN_KIND } from "./dyn.js";
import { rawBytes } from "./lib-abi.js";
@@ -184,8 +189,8 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
- if (host.wasi && e.fn === "async.hop") {
- host.emitWasiSuspend(null);
+ if (host.coroAwait && e.fn === "async.hop") {
+ host.emitCoroAwait(null);
return { name: "", type: e.type };
}
if (host.wasi && e.fn === "async.awaitDyn") {
@@ -431,7 +436,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) {
throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
}
- const sym = LIB_FN_SYMS[e.fn];
+ const sym = mockTimerSym(host, e) ?? LIB_FN_SYMS[e.fn];
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
? emitBorrowedInputs(host, e.args)
@@ -680,6 +685,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "events":
case "blob":
case "record":
+ case "disposable":
return host.emitGenericLibCall(e);
case "test":
return emitTestLibCall(host, e);
diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts
index bcb65c8fa..74278789c 100644
--- a/packages/compiler/src/backend/llvm/lib-filesystem.ts
+++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts
@@ -746,6 +746,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
const dict = B.tmp();
B.line(`${dict} = call ptr @${mangleRecordNew(e.type.shapeId)}()`);
const out = host.own({ name: dict, type: e.type });
+ // Node builds the result with a null prototype.
+ B.line(`store i8 1, ptr ${host.recordMapFlagsPtr(dict, e.type.shapeId)}`);
const ovf = host.recordOvfPtr(dict, e.type.shapeId);
B.line(
`call void @scr_qs_parse_into(ptr ${ovf}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, double ${args[3]!.name}, i32 ${strTag}, i32 ${arrTag})`,
@@ -1034,6 +1036,16 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.line(`${t} = sitofp i32 ${product} to double`);
return { name: t, type: e.type, uint32: product };
}
+ if (e.fn === "math.f16round") {
+ const v = host.emitExpr(e.args[0]!);
+ host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`);
+ host.declare(`declare double @scr_f16_to_f64(i16 zeroext)`);
+ const narrowed = B.tmp();
+ const t = B.tmp();
+ B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${v.name})`);
+ B.line(`${t} = call double @scr_f16_to_f64(i16 zeroext ${narrowed})`);
+ return { name: t, type: e.type };
+ }
if (e.fn === "math.fround") {
const v = host.emitExpr(e.args[0]!);
const narrowed = B.tmp();
diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts
index 46d08a58f..b30c65b8d 100644
--- a/packages/compiler/src/backend/llvm/lib-process.ts
+++ b/packages/compiler/src/backend/llvm/lib-process.ts
@@ -4,6 +4,7 @@ import { undefinedArmTag } from "../../ir/analysis.js";
import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js";
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
+import { mockTimerSym } from "./lib-shared.js";
export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
@@ -372,14 +373,16 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
host.usesTimers = true;
const cb = host.emitExpr(e.args[0]!);
host.moveTemp(cb);
- const sym = {
- "timers.setTimeout": "scr_set_timeout",
- "timers.setInterval": "scr_set_interval",
- "timers.setTimeoutHandle": "scr_set_timeout_handle",
- "timers.setImmediate": "scr_set_immediate",
- "process.nextTick": "scr_next_tick",
- "timers.queueMicrotask": "scr_queue_microtask",
- }[e.fn];
+ const sym =
+ mockTimerSym(host, e) ??
+ {
+ "timers.setTimeout": "scr_set_timeout",
+ "timers.setInterval": "scr_set_interval",
+ "timers.setTimeoutHandle": "scr_set_timeout_handle",
+ "timers.setImmediate": "scr_set_immediate",
+ "process.nextTick": "scr_next_tick",
+ "timers.queueMicrotask": "scr_queue_microtask",
+ }[e.fn];
const rest = e.args.slice(1).map((a) => host.emitExpr(a));
const argList = [`ptr ${cb.name}`, ...rest.map((a) => `${host.llType(a.type)} ${a.name}`)].join(
", ",
@@ -405,21 +408,24 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
// The chaining forms: bookkeep, then yield the handle itself (the
// C comma expressions).
const h = host.emitExpr(e.args[0]!);
- const sym = {
- "timers.unref": "scr_timer_unref",
- "timers.ref": "scr_timer_ref",
- "timers.refresh": "scr_timer_refresh",
- "timers.immediateUnref": "scr_immediate_unref",
- "timers.immediateRef": "scr_immediate_ref",
- }[e.fn];
+ const sym =
+ mockTimerSym(host, e) ??
+ {
+ "timers.unref": "scr_timer_unref",
+ "timers.ref": "scr_timer_ref",
+ "timers.refresh": "scr_timer_refresh",
+ "timers.immediateUnref": "scr_immediate_unref",
+ "timers.immediateRef": "scr_immediate_ref",
+ }[e.fn];
host.declare(`declare void @${sym}(double)`);
B.line(`call void @${sym}(double ${h.name})`);
return { name: h.name, type: e.type };
}
if (e.fn === "timers.clearTimeout" || e.fn === "timers.clearInterval") {
const h = host.emitExpr(e.args[0]!);
- host.declare(`declare void @scr_clear_interval(double)`);
- B.line(`call void @scr_clear_interval(double ${h.name})`);
+ const sym = mockTimerSym(host, e) ?? "scr_clear_interval";
+ host.declare(`declare void @${sym}(double)`);
+ B.line(`call void @${sym}(double ${h.name})`);
return { name: "", type: e.type };
}
if (e.fn === "timers.clearNoop") {
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 168625f92..6a85a8eb3 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -37,7 +37,7 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap = new Ma
export const LIB_FN_SYMS: Record = {
"assert.expectsErrDyn": "scr_assert_expects_err_dyn",
- "insp.jsonDyn": "scr_dyn_format_j",
+ "insp.jsonDyn": "scr_util_format_j",
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
"intl.segmenterNew": "scr_intl_segmenter_new",
"weakMap.is": "scr_weak_map_is",
@@ -65,6 +65,7 @@ export const LIB_FN_SYMS: Record = {
"arrayBuffer.viewU8": "scr_array_buffer_view_u8",
"arrayBuffer.viewU32": "scr_array_buffer_view_u32",
"arrayBuffer.viewI32": "scr_array_buffer_view_i32",
+ "arrayBuffer.viewF16": "scr_array_buffer_view_f16",
"arrayBuffer.viewF32": "scr_array_buffer_view_f32",
"arrayBuffer.viewF64": "scr_array_buffer_view_f64",
"arrayBuffer.viewDV": "scr_array_buffer_view_dv",
@@ -188,6 +189,7 @@ export const LIB_FN_SYMS: Record = {
"os.uptime": "scr_os_uptime",
"os.loadavg": "scr_os_loadavg",
"os.getPriority": "scr_os_get_priority",
+ "os.setPriority": "scr_os_set_priority",
"os.cpusTake": "scr_os_cpus_take",
"os.cpusModel": "scr_os_cpus_model",
"os.cpusField": "scr_os_cpus_field",
@@ -198,6 +200,7 @@ export const LIB_FN_SYMS: Record = {
"os.userHomedir": "scr_os_user_homedir",
"os.tmpdir": "scr_os_tmpdir",
"process.argv": "scr_process_argv",
+ "process.execArgv": "scr_process_exec_argv",
"process.platform": "scr_process_platform",
"process.cwd": "scr_process_cwd",
"process.pid": "scr_process_pid",
@@ -477,6 +480,7 @@ export const LIB_FN_SYMS: Record = {
"strdec.write": "scr_strdec_write",
"strdec.next": "scr_strdec_next",
"strdec.end": "scr_strdec_end",
+ "strdec.encoding": "scr_strdec_encoding",
"stats.isFile": "scr_stats_is_file",
"stats.isDirectory": "scr_stats_is_dir",
"stats.isSymbolicLink": "scr_stats_is_symlink",
@@ -575,14 +579,17 @@ export const LIB_FN_SYMS: Record = {
"process.stdoutWriteBytes": "scr_process_stdout_write_bytes",
"process.stderrWriteBytes": "scr_process_stderr_write_bytes",
"insp.buffer": "scr_insp_buffer",
+ "insp.typedArray": "scr_insp_typed_array",
// util.inspect (scr_inspect.c): all args borrowed; string results +1;
// the begin/entry/key/moreItems/end accumulator drives the emitted
// container walks. None of these throw.
"insp.f64": "scr_insp_f64",
"insp.str": "scr_insp_str",
"insp.regex": "scr_insp_regex",
+ "insp.date": "scr_insp_date",
"insp.error": "scr_insp_error",
"insp.begin": "scr_insp_begin",
+ "insp.layout": "scr_insp_layout",
"insp.entry": "scr_insp_entry",
"insp.key": "scr_insp_key",
"insp.moreItems": "scr_insp_more_items",
@@ -894,6 +901,7 @@ export const LIB_FN_SYMS: Record = {
"date.checkedValue": "scr_dyn_native_date_value",
"dyn.nativeRegexIs": "scr_dyn_native_regex_is",
"dyn.toString": "scr_dyn_to_string_argument",
+ "dyn.valueOf": "scr_dyn_value_of",
"dyn.dataViewIs": "scr_dyn_data_view_is",
"dyn.construct": "scr_dyn_construct",
"dyn.this": "scr_dyn_this_get",
@@ -930,8 +938,18 @@ export const LIB_FN_SYMS: Record = {
"worker.root": "scr_worker_root",
"worker.data": "scr_worker_data",
"worker.parentPort": "scr_worker_parent_port",
+ "worker.resourceLimits": "scr_worker_resource_limits",
+ "worker.threadName": "scr_worker_thread_name_current",
"worker.channel": "scr_worker_message_channel",
"worker.receiveMessageOnPort": "scr_worker_receive_message_on_port",
+ "worker.setEnvironmentData": "scr_worker_set_environment_data",
+ "worker.getEnvironmentData": "scr_worker_get_environment_data",
+ "worker.markUntransferable": "scr_worker_mark_untransferable",
+ "worker.isMarkedUntransferable": "scr_worker_is_marked_untransferable",
+ "dyn.markUncloneable": "scr_mark_uncloneable",
+ "stream.workerStdio": "scr_stream_worker_stdio",
+ "worker.broadcastChannel": "scr_worker_broadcast_channel",
+ "event.broadcastInstall": "scr_et_broadcast_install",
"process.stdinDestroy": "scr_process_stdin_destroy",
"process.stdinSetRawMode": "scr_process_stdin_set_raw_mode",
// node:events EventEmitter (scr_events_emitter.c): receivers borrowed,
@@ -1091,6 +1109,12 @@ export const LIB_FN_SYMS: Record = {
"net.serverMaxConnectionsSet": "scr_net_server_max_connections_set",
"net.serverDropMaxConnectionGet": "scr_net_server_drop_max_connection_get",
"net.serverDropMaxConnectionSet": "scr_net_server_drop_max_connection_set",
+ "net.socketAddressNew": "scr_net_socket_address_new",
+ "net.socketAddressParse": "scr_net_socket_address_parse",
+ "net.isSocketAddress": "scr_net_is_socket_address",
+ "net.blockListNew": "scr_net_block_list_new",
+ "net.isBlockList": "scr_net_is_block_list",
+ "net.blockListPrivateRanges": "scr_net_block_list_private_ranges",
"net.serverOnDrop": "scr_net_server_on_drop",
"net.serverGetConnections": "scr_net_server_get_connections",
"net.sockReadable": "scr_net_sock_readable",
@@ -1216,9 +1240,14 @@ export const LIB_FN_SYMS: Record = {
// loop gate already runs) and diagnostics_channel (scr_dc.c): all
// generic shapes; the throwing members ride the pending check.
"tp.setTimeout": "scr_tp_set_timeout",
+ "tp.schedulerWait": "scr_tp_set_timeout",
"tp.setImmediate": "scr_tp_set_immediate",
"tp.setTimeoutValue": "scr_tp_set_timeout_value",
"tp.setImmediateValue": "scr_tp_set_immediate_value",
+ "tp.setTimeoutSignal": "scr_tp_set_timeout_signal",
+ "tp.schedulerWaitValue": "scr_tp_set_timeout_value",
+ "tp.schedulerWaitSignal": "scr_tp_set_timeout_signal",
+ "tp.setImmediateSignal": "scr_tp_set_immediate_signal",
// stream/promises' finished form (+1 promise; may-throw pending check;
// the pipeline form needs the stream-array compound and emits below).
"sp.finished": "scr_sp_finished",
@@ -1315,6 +1344,9 @@ export const LIB_FN_SYMS: Record = {
"dyn.reflectGet": "scr_dyn_reflect_get",
"dyn.usingMethod": "scr_dyn_using_method",
"dyn.usingDispose": "scr_dyn_using_dispose",
+ "disposable.stackNew": "scr_disposable_stack_new",
+ "disposable.stackIs": "scr_disposable_stack_is",
+ "disposable.stackUseCallback": "scr_disposable_stack_use_callback",
"dyn.reflectSet": "scr_dyn_reflect_set",
"dyn.reflectDefine": "scr_dyn_reflect_define",
"dyn.abstractEq": "scr_dyn_abstract_eq",
@@ -1367,6 +1399,40 @@ export const LIB_FN_SYMS: Record = {
"timers.immediatePromise": "scr_immediate_promise",
};
+/** The mock-aware entry points (scr_test.c) that the timer calls of a
+ * program using mock.timers emit in place of the loop's functions; each
+ * falls back to the loop's function while its API is not mocked. */
+export const MOCK_TIMER_SYMS: Readonly> = {
+ "timers.setTimeout": "scr_test_mt_set_timeout",
+ "timers.setTimeoutHandle": "scr_test_mt_set_timeout_handle",
+ "timers.setInterval": "scr_test_mt_set_interval",
+ "timers.clearTimeout": "scr_test_mt_clear_timeout",
+ "timers.clearInterval": "scr_test_mt_clear_interval",
+ "timers.unref": "scr_test_mt_unref",
+ "timers.ref": "scr_test_mt_ref",
+ "timers.refresh": "scr_test_mt_refresh",
+ "timers.hasRef": "scr_test_mt_has_ref",
+ "timers.setImmediate": "scr_test_mt_set_immediate",
+ "timers.clearImmediate": "scr_test_mt_clear_immediate",
+ "timers.immediateUnref": "scr_test_mt_immediate_unref",
+ "timers.immediateRef": "scr_test_mt_immediate_ref",
+ "timers.immediateHasRef": "scr_test_mt_immediate_has_ref",
+ "date.now": "scr_test_mt_date_now",
+ "date.newNow": "scr_test_mt_date_now",
+ "tp.schedulerWait": "scr_test_mt_scheduler_wait",
+ "tp.schedulerWaitValue": "scr_test_mt_scheduler_wait_value",
+ "tp.schedulerWaitSignal": "scr_tp_scheduler_wait_signal",
+};
+
+/** The mock-aware symbol for a timer call of a program that uses
+ * mock.timers, undefined for other calls and for `realTimer` calls. */
+export function mockTimerSym(
+ host: { readonly mockTimers: boolean },
+ e: { fn: string; realTimer?: true },
+): string | undefined {
+ return host.mockTimers && !e.realTimer ? MOCK_TIMER_SYMS[e.fn] : undefined;
+}
+
export const USES_TIMERS_LIB_FNS = new Set([
"process.emitWarning",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
@@ -1384,6 +1450,7 @@ export const USES_TIMERS_LIB_FNS = new Set([
"worker.parentPort",
"worker.channel",
"worker.receiveMessageOnPort",
+ "worker.broadcastChannel",
"process.stdio",
"fetch.start",
"fetch.input",
diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts
index 79590cd9d..d0d9a9bfc 100644
--- a/packages/compiler/src/backend/llvm/lib-test.ts
+++ b/packages/compiler/src/backend/llvm/lib-test.ts
@@ -66,6 +66,34 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
return finishRuntimeCall(host, e, "scr_test_ctx_mock_restore_all", a);
case "test.ctxMockReset":
return finishRuntimeCall(host, e, "scr_test_ctx_mock_reset", a);
+ case "test.snapshotValue":
+ return finishRuntimeCall(host, e, "scr_test_snapshot_value", a);
+ case "test.ctxSnapshot":
+ return finishRuntimeCall(host, e, "scr_test_ctx_snapshot", a);
+ case "test.fileSnapshot":
+ return finishRuntimeCall(host, e, "scr_test_file_snapshot", a);
+ case "test.snapshotSerialize":
+ return finishRuntimeCall(host, e, "scr_test_snapshot_serialize", a);
+ case "test.mockTimersEnable":
+ return finishRuntimeCall(host, e, "scr_test_mock_timers_enable", a);
+ case "test.mockTimersTick":
+ return finishRuntimeCall(host, e, "scr_test_mock_timers_tick", a);
+ case "test.mockTimersRunAll":
+ return finishRuntimeCall(host, e, "scr_test_mock_timers_run_all", a);
+ case "test.mockTimersReset":
+ return finishRuntimeCall(host, e, "scr_test_mock_timers_reset", a);
+ case "test.mockTimersSetTime":
+ return finishRuntimeCall(host, e, "scr_test_mock_timers_set_time", a);
+ case "test.ctxMockTimersEnable":
+ return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_enable", a);
+ case "test.ctxMockTimersTick":
+ return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_tick", a);
+ case "test.ctxMockTimersRunAll":
+ return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_run_all", a);
+ case "test.ctxMockTimersReset":
+ return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_reset", a);
+ case "test.ctxMockTimersSetTime":
+ return finishRuntimeCall(host, e, "scr_test_ctx_mock_timers_set_time", a);
default:
throw new InternalCompilerError(`unhandled test call ${e.fn}`);
}
diff --git a/packages/compiler/src/backend/llvm/local-extents.test.ts b/packages/compiler/src/backend/llvm/local-extents.test.ts
new file mode 100644
index 000000000..ae3b976e2
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/local-extents.test.ts
@@ -0,0 +1,96 @@
+import { expect, test } from "vitest";
+import { BOOL, F64, arrayOf, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
+import { lastUseRead, storedLastUseRead } from "./local-extents.js";
+
+const loc = { file: "extents.ts", start: 0, end: 0 };
+const cell: IrType = { kind: "object", className: "Cell" };
+const cells = arrayOf(cell);
+const ref = (localId: string, type: IrType = cell): IrExpr => ({
+ kind: "varRef",
+ localId,
+ type,
+ loc,
+});
+const decl = (localId: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId, init, loc });
+const push = (arr: string, value: IrExpr): IrStmt => ({
+ kind: "exprStmt",
+ expr: {
+ kind: "arrIntrinsic",
+ method: "push",
+ receiver: ref(arr, cells),
+ args: [value],
+ type: F64,
+ loc,
+ },
+ loc,
+});
+const field = (obj: string, name: string): IrExpr => ({
+ kind: "fieldGet",
+ obj: ref(obj),
+ className: "Cell",
+ field: name,
+ type: cell,
+ loc,
+});
+const use = (value: IrExpr): IrStmt => ({
+ kind: "exprStmt",
+ expr: { kind: "call", callee: "use", args: [value], type: F64, loc },
+ loc,
+});
+const fresh: IrExpr = { kind: "new", className: "Cell", args: [], type: cell, loc };
+
+test("the only read in the last statement of a binding's extent moves", () => {
+ const read = ref("row");
+ const stmts = [decl("row", fresh), use(field("row", "next")), push("out", read)];
+ expect(lastUseRead(stmts, 0, "row")).toBe(read);
+ expect(storedLastUseRead(stmts, 0, "row")).toBe(read);
+ // A call argument is a last use but not a store.
+ const arg = ref("row");
+ const called = [decl("row", fresh), use(arg)];
+ expect(lastUseRead(called, 0, "row")).toBe(arg);
+ expect(storedLastUseRead(called, 0, "row")).toBeNull();
+});
+
+test("a derived alias extends the extent past the move", () => {
+ const stmts = [decl("row", fresh), decl("next", field("row", "next")), push("out", ref("row"))];
+ // The alias is never mentioned later: the push is still the last use.
+ expect(lastUseRead(stmts, 0, "row")).not.toBeNull();
+ stmts.push(use(ref("next")));
+ expect(lastUseRead(stmts, 0, "row")).toBeNull();
+});
+
+test("repeated reads, loops and try statements keep the reference", () => {
+ const twice: IrStmt[] = [
+ decl("row", fresh),
+ {
+ kind: "exprStmt",
+ expr: { kind: "call", callee: "pair", args: [ref("row"), ref("row")], type: F64, loc },
+ loc,
+ },
+ ];
+ expect(lastUseRead(twice, 0, "row")).toBeNull();
+ const looped: IrStmt[] = [
+ decl("row", fresh),
+ {
+ kind: "while",
+ cond: { kind: "boolLit", value: true, type: BOOL, loc },
+ body: [push("out", ref("row"))],
+ loc,
+ },
+ ];
+ expect(lastUseRead(looped, 0, "row")).toBeNull();
+});
+
+test("parameters count rebound locals as derived only when asked", () => {
+ // let n = head; n = n.next; this.head = head; use(n)
+ const body: IrStmt[] = [
+ decl("n", ref("head")),
+ { kind: "assign", localId: "n", value: field("n", "next"), loc },
+ push("out", ref("head")),
+ use(ref("n")),
+ ];
+ // `n` is assigned, so the declaration alone does not derive it...
+ expect(lastUseRead(body, -1, "head")).not.toBeNull();
+ // ...but a walk could borrow from the parameter through it.
+ expect(lastUseRead(body, -1, "head", true)).toBeNull();
+});
diff --git a/packages/compiler/src/backend/llvm/local-extents.ts b/packages/compiler/src/backend/llvm/local-extents.ts
new file mode 100644
index 000000000..7706a9c0a
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/local-extents.ts
@@ -0,0 +1,207 @@
+import type { IrExpr, IrStmt } from "../../ir/ir.js";
+import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
+
+/** The only read of an owned binding in the last statement of its extent,
+ * when that read can take over the binding's reference: the statement reads
+ * the binding exactly once, outside any loop or try statement nested in it,
+ * assigns it nowhere and mentions no binding derived from it (such a
+ * borrowed alias could depend on the moved reference). After the read no
+ * statement observes the binding again, so the slot can be cleared instead
+ * of retaining the value (scope releases tolerate NULL).
+ *
+ * The binding is declared at `stmts[index]`, or before the list starts when
+ * `index` is -1 (a parameter and its function body). Callers exclude boxed
+ * bindings and must also check that the read node occurs once in the
+ * function: lowering may share one varRef node between several reads.
+ * `rebound` also counts rebound locals as derived (walk locals can borrow
+ * from a parameter while rebinding). */
+export function lastUseRead(
+ stmts: readonly IrStmt[],
+ index: number,
+ localId: string,
+ rebound = false,
+): IrExpr | null {
+ return lastUse(stmts, index, localId, rebound)?.read ?? null;
+}
+
+/** Stores that keep a moved value: the read is the stored value of a field,
+ * record or element store, a record literal field, or a pushed element. */
+export function storedLastUseRead(
+ stmts: readonly IrStmt[],
+ index: number,
+ localId: string,
+ rebound = false,
+): IrExpr | null {
+ const use = lastUse(stmts, index, localId, rebound);
+ return use !== null && use.stored ? use.read : null;
+}
+
+function lastUse(
+ stmts: readonly IrStmt[],
+ index: number,
+ localId: string,
+ rebound: boolean,
+): { read: IrExpr; stored: boolean } | null {
+ const { extent, derived } = extentOf(stmts, index, localId, rebound);
+ const last = extent[extent.length - 1];
+ if (last === undefined) return null;
+ const state: { read: IrExpr | null; stored: boolean; ok: boolean } = {
+ read: null,
+ stored: false,
+ ok: true,
+ };
+ const isRead = (e: IrExpr): boolean => e.kind === "varRef" && e.localId === localId;
+ const stmt = (s: IrStmt): boolean => {
+ if (!state.ok) return false;
+ if ((s.kind === "varDecl" || s.kind === "assign") && derived.has(s.localId)) {
+ state.ok = false;
+ return false;
+ }
+ if (
+ s.kind === "for" ||
+ s.kind === "forOf" ||
+ s.kind === "while" ||
+ s.kind === "doWhile" ||
+ s.kind === "tryCatch"
+ ) {
+ if (!absentStmt(s, derived)) state.ok = false;
+ return state.ok;
+ }
+ if (
+ (s.kind === "fieldSet" || s.kind === "recordSet" || s.kind === "arraySet") &&
+ isRead(s.value)
+ )
+ state.stored = true;
+ return everyStmtChild(s, expr, stmt);
+ };
+ const expr = (e: IrExpr): boolean => {
+ if (!state.ok) return false;
+ if (isRead(e)) {
+ if (state.read !== null) state.ok = false;
+ else state.read = e;
+ return state.ok;
+ }
+ if (e.kind === "varRef" || e.kind === "assignExpr" || e.kind === "incDec") {
+ if (derived.has(e.localId)) state.ok = false;
+ } else if (e.kind === "closure" || e.kind === "classRef") {
+ for (const id of e.captures ?? []) if (derived.has(id)) state.ok = false;
+ } else if (e.kind === "recordLit") {
+ if (e.fields.some((f) => isRead(f.value))) state.stored = true;
+ } else if (e.kind === "arrIntrinsic" && e.method === "push") {
+ if (e.args.some(isRead)) state.stored = true;
+ }
+ if (!state.ok) return false;
+ return everyExprChild(e, expr, stmt);
+ };
+ stmt(last);
+ return state.ok && state.read !== null ? { read: state.read, stored: state.stored } : null;
+}
+
+/** The statements of a block during which a binding declared at
+ * `stmts[index]` can still be observed: from the declaration through the
+ * last later statement that mentions the binding or any binding derived
+ * from it. A binding is derived when its declaration (or loop binding)
+ * mentions the binding or another derived one: a borrowed projection of a
+ * borrowed binding (`const b = a.f`) stays valid only while the first
+ * binding's own source keeps its value. Closure captures count as
+ * mentions. */
+function extentOf(
+ stmts: readonly IrStmt[],
+ index: number,
+ localId: string,
+ rebound: boolean,
+): { extent: IrStmt[]; derived: ReadonlySet } {
+ const rest = stmts.slice(index + 1);
+ const derived = new Set([localId]);
+ const assigned = new Set();
+ if (!rebound) for (const s of rest) collectAssignedStmt(s, assigned);
+ // Declarations of other bindings that mention the derived set join it;
+ // repeat until nothing is added (loops can declare backwards).
+ for (;;) {
+ const before = derived.size;
+ for (const s of rest) collectDerivedStmt(s, derived, assigned);
+ if (derived.size === before) break;
+ }
+ let last = -1;
+ for (let i = 0; i < rest.length; i++) if (!absentStmt(rest[i]!, derived)) last = i;
+ return { extent: rest.slice(0, last + 1), derived };
+}
+
+/** Only bindings that are never assigned can hold a borrowed pointer
+ * derived from another local: an assigned local owns its value (walk
+ * locals root only at parameters, which own nothing to move). */
+function collectDerivedStmt(
+ s: IrStmt,
+ derived: Set,
+ assigned: ReadonlySet,
+): boolean {
+ if (
+ (s.kind === "varDecl" || s.kind === "forOf" || s.kind === "assign") &&
+ !derived.has(s.localId) &&
+ !assigned.has(s.localId)
+ ) {
+ const value = s.kind === "varDecl" ? s.init : s.kind === "assign" ? s.value : s.iterable;
+ if (value !== null && !absentExpr(value, derived)) derived.add(s.localId);
+ }
+ return everyStmtChild(
+ s,
+ (e) => collectDerivedExpr(e, derived, assigned),
+ (c) => collectDerivedStmt(c, derived, assigned),
+ );
+}
+
+function collectDerivedExpr(
+ e: IrExpr,
+ derived: Set,
+ assigned: ReadonlySet,
+): boolean {
+ if (e.kind === "assignExpr" && !derived.has(e.localId) && !assigned.has(e.localId)) {
+ if (!absentExpr(e.value, derived)) derived.add(e.localId);
+ }
+ return everyExprChild(
+ e,
+ (c) => collectDerivedExpr(c, derived, assigned),
+ (s) => collectDerivedStmt(s, derived, assigned),
+ );
+}
+
+function collectAssignedStmt(s: IrStmt, assigned: Set): boolean {
+ if (s.kind === "assign") assigned.add(s.localId);
+ return everyStmtChild(
+ s,
+ (e) => collectAssignedExpr(e, assigned),
+ (c) => collectAssignedStmt(c, assigned),
+ );
+}
+
+function collectAssignedExpr(e: IrExpr, assigned: Set): boolean {
+ if (e.kind === "assignExpr" || e.kind === "incDec") assigned.add(e.localId);
+ return everyExprChild(
+ e,
+ (c) => collectAssignedExpr(c, assigned),
+ (s) => collectAssignedStmt(s, assigned),
+ );
+}
+
+/** True while no node of the tree mentions a derived binding. */
+function absentExpr(e: IrExpr, derived: ReadonlySet): boolean {
+ if (e.kind === "varRef" || e.kind === "assignExpr" || e.kind === "incDec") {
+ if (derived.has(e.localId)) return false;
+ } else if (e.kind === "closure" || e.kind === "classRef") {
+ for (const id of e.captures ?? []) if (derived.has(id)) return false;
+ }
+ return everyExprChild(
+ e,
+ (c) => absentExpr(c, derived),
+ (s) => absentStmt(s, derived),
+ );
+}
+
+function absentStmt(s: IrStmt, derived: ReadonlySet): boolean {
+ if ((s.kind === "varDecl" || s.kind === "assign") && derived.has(s.localId)) return false;
+ return everyStmtChild(
+ s,
+ (e) => absentExpr(e, derived),
+ (c) => absentStmt(c, derived),
+ );
+}
diff --git a/packages/compiler/src/backend/llvm/reference-writes.test.ts b/packages/compiler/src/backend/llvm/reference-writes.test.ts
new file mode 100644
index 000000000..cbbe49d3b
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/reference-writes.test.ts
@@ -0,0 +1,99 @@
+import { expect, test } from "vitest";
+import {
+ F64,
+ arrayOf,
+ type IrExpr,
+ type IrFunction,
+ type IrStmt,
+ type IrType,
+} from "../../ir/ir.js";
+import { ReferenceWrites } from "./reference-writes.js";
+
+const loc = { file: "writes.ts", start: 0, end: 0 };
+const cell: IrType = { kind: "object", className: "Cell" };
+const cells = arrayOf(cell);
+const ref = (localId: string, type: IrType = cells): IrExpr => ({
+ kind: "varRef",
+ localId,
+ type,
+ loc,
+});
+const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
+const element = (arr: string): IrExpr => ({
+ kind: "arrayGet",
+ arr: ref(arr),
+ index: num(0),
+ type: cell,
+ loc,
+});
+const store = (arr: string, value: IrExpr): IrStmt => ({
+ kind: "arraySet",
+ arr: ref(arr),
+ index: { kind: "arrIntrinsic", method: "length", receiver: ref(arr), args: [], type: F64, loc },
+ value,
+ loc,
+});
+
+/** fill(input): const out = []; out[out.length] = input[0]; ...; return out */
+function fill(name: string, extra: IrStmt[]): IrFunction {
+ return {
+ name,
+ loc,
+ returnType: cells,
+ params: [{ localId: "input", name: "input", type: cells }],
+ locals: [
+ { id: "input", name: "input", type: cells, mutable: false },
+ { id: "out", name: "out", type: cells, mutable: false },
+ { id: "x", name: "x", type: cell, mutable: false },
+ ],
+ body: [
+ {
+ kind: "varDecl",
+ localId: "out",
+ init: { kind: "arrayLit", elems: [], type: cells, loc },
+ loc,
+ },
+ store("out", element("input")),
+ ...extra,
+ { kind: "return", value: ref("out"), loc },
+ ],
+ };
+}
+
+function writes(fn: IrFunction): ReferenceWrites {
+ return new ReferenceWrites(new Map([[fn.name, fn]]), [], () => false, new Set());
+}
+
+test("stores into a function's own unreached array write no shared location", () => {
+ const fn = fill("fill", []);
+ const rw = writes(fn);
+ expect(rw.summary("fill")).toBeNull();
+ rw.enter(fn);
+ expect(rw.stmts(fn.body)).toBeNull();
+});
+
+test("an array that is read back or escapes keeps its element writes", () => {
+ const readBack = fill("readBack", [{ kind: "varDecl", localId: "x", init: element("out"), loc }]);
+ const rw = writes(readBack);
+ expect(rw.summary("readBack")).not.toBeNull();
+ expect(rw.conflicts(["a"], rw.summary("readBack"))).toBe(true);
+ // Storing the array somewhere else lets another path reach its slots.
+ const escaped = fill("escaped", [
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "arrIntrinsic",
+ method: "push",
+ receiver: ref("input"),
+ args: [ref("out")],
+ type: F64,
+ loc,
+ },
+ loc,
+ },
+ ]);
+ expect(writes(escaped).conflicts(["a"], writes(escaped).summary("escaped"))).toBe(true);
+ // Writing the input array is never private.
+ const input = fill("input", [store("input", element("input"))]);
+ expect(writes(input).conflicts(["a"], writes(input).summary("input"))).toBe(true);
+});
diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts
index dd151de01..2371147a9 100644
--- a/packages/compiler/src/backend/llvm/reference-writes.ts
+++ b/packages/compiler/src/backend/llvm/reference-writes.ts
@@ -106,6 +106,7 @@ const INERT_LIB_CALLS = new Set([
"regex.groupParseInt",
"process.cwd",
"process.argv",
+ "process.execArgv",
"process.platform",
"process.execPath",
"process.isTTY",
@@ -191,6 +192,7 @@ export class ReferenceWrites {
private current: IrFunction | null = null;
private currentBoxed = new Set();
private currentLocals = new Set();
+ private currentPrivateArrays: ReadonlySet = new Set();
private constructors: ReadonlySet = new Set();
private inConstructor = false;
@@ -278,6 +280,7 @@ export class ReferenceWrites {
}
const thisId = external && constructors.has(fn.name) ? (fn.params[0]?.localId ?? null) : null;
const boxed = boxedLocals(fn);
+ const privates = privateArrays(fn, boxed);
const sink: WriteSink = {
add: (k) => {
// Interned for queries; a callee's locals are private to it.
@@ -311,7 +314,7 @@ export class ReferenceWrites {
const stmt = (s: IrStmt): boolean => {
if (thisId !== null && s.kind === "fieldSet" && isThisRef(s.obj, thisId)) {
// A store into the fresh `this`: no outside borrow can reach it.
- } else this.stmtWrites(s, sink, boxed);
+ } else this.stmtWrites(s, sink, boxed, privates);
return everyStmtChild(s, expr, stmt);
};
fn.body.every(stmt);
@@ -574,7 +577,12 @@ export class ReferenceWrites {
}
/** Writes of one statement node itself. */
- private stmtWrites(s: IrStmt, sink: WriteSink, boxed: ReadonlySet): void {
+ private stmtWrites(
+ s: IrStmt,
+ sink: WriteSink,
+ boxed: ReadonlySet,
+ privates: ReadonlySet,
+ ): void {
switch (s.kind) {
case "assign":
if (isRefCounted(s.value.type)) sink.add(this.localKey(s.localId, boxed));
@@ -593,7 +601,7 @@ export class ReferenceWrites {
case "arraySetUndefined":
case "arrayDelete":
case "arraySetLength":
- sink.add(ARRAY_KEY);
+ if (!privateReceiver(s.arr, privates)) sink.add(ARRAY_KEY);
return;
case "varDecl":
case "exprStmt":
@@ -701,6 +709,7 @@ export class ReferenceWrites {
enter(fn: IrFunction): void {
this.current = fn;
this.currentBoxed = boxedLocals(fn);
+ this.currentPrivateArrays = privateArrays(fn, this.currentBoxed);
this.currentLocals = new Set(fn.locals.map((local) => local.id));
this.inConstructor = this.constructors.has(fn.name);
for (const p of fn.params) this.currentLocals.add(p.localId);
@@ -780,7 +789,7 @@ export class ReferenceWrites {
const known = this.stmtCache.get(s);
if (known !== undefined) return known;
const parts: Writes[] = [];
- this.stmtWrites(s, this.querySink(parts), this.currentBoxed);
+ this.stmtWrites(s, this.querySink(parts), this.currentBoxed, this.currentPrivateArrays);
everyStmtChild(
s,
(child) => (parts.push(this.expr(child)), true),
@@ -915,6 +924,96 @@ function isThisRef(e: IrExpr, thisId: string): boolean {
return false;
}
+/** A write whose receiver is a private array of the current function. */
+function privateReceiver(arr: IrExpr, privates: ReadonlySet): boolean {
+ return privates.size > 0 && arr.kind === "varRef" && privates.has(arr.localId);
+}
+
+/** Array methods a private array may receive: they neither read an
+ * element out nor let the array escape. */
+const PRIVATE_ARRAY_METHODS = new Set(["length", "push"]);
+
+/** Array locals whose storage the function allocated itself and that
+ * nothing else can reach before the function returns them: declared once
+ * from an array literal or `new Array(n)`, never rebound, unboxed, and
+ * otherwise only the receiver of element stores, length updates, `length`,
+ * `push` and presence tests, or the value of a `return`. No code reads an
+ * element of such an array or reaches its storage through another path, so
+ * its element stores cannot release a value that a borrow depends on:
+ * a borrow's own location stays unwritten, and the array is not one. The
+ * stores are private to the activation, like writes of its locals, and
+ * stay out of the function's summary (a caller's borrows predate the
+ * array). The literal map, filter and fused-pipeline helpers fill their
+ * results this way. */
+function privateArrays(fn: IrFunction, boxed: ReadonlySet): ReadonlySet {
+ if (fn.async || fn.generator) return new Set();
+ const candidates = new Set();
+ for (const local of fn.locals)
+ if (local.type.kind === "array" && !local.boxed && !boxed.has(local.id))
+ candidates.add(local.id);
+ if (candidates.size === 0) return candidates;
+ const declarations = new Map();
+ const rejected = new Set();
+ // Permitted positions per varRef node (lowering may share one node
+ // between several positions, so each visit consumes one permission).
+ const permitted = new Map();
+ const permit = (e: IrExpr): void => {
+ if (e.kind === "varRef" && candidates.has(e.localId))
+ permitted.set(e, (permitted.get(e) ?? 0) + 1);
+ };
+ const expr = (e: IrExpr): boolean => {
+ switch (e.kind) {
+ case "varRef": {
+ const left = permitted.get(e) ?? 0;
+ if (left === 0) rejected.add(e.localId);
+ else permitted.set(e, left - 1);
+ break;
+ }
+ case "assignExpr":
+ case "incDec":
+ rejected.add(e.localId);
+ break;
+ case "closure":
+ case "classRef":
+ for (const id of e.captures ?? []) rejected.add(id);
+ break;
+ case "arrIntrinsic":
+ if (PRIVATE_ARRAY_METHODS.has(e.method)) permit(e.receiver);
+ break;
+ case "arrayHas":
+ permit(e.arr);
+ break;
+ }
+ return everyExprChild(e, expr, stmt);
+ };
+ const stmt = (s: IrStmt): boolean => {
+ switch (s.kind) {
+ case "varDecl":
+ declarations.set(s.localId, (declarations.get(s.localId) ?? 0) + 1);
+ if (s.init === null || (s.init.kind !== "arrayLit" && s.init.kind !== "arrayNewLen"))
+ rejected.add(s.localId);
+ break;
+ case "assign":
+ rejected.add(s.localId);
+ break;
+ case "arraySet":
+ case "arraySetUndefined":
+ case "arrayDelete":
+ case "arraySetLength":
+ permit(s.arr);
+ break;
+ case "return":
+ if (s.value !== null) permit(s.value);
+ break;
+ }
+ return everyStmtChild(s, expr, stmt);
+ };
+ fn.body.every(stmt);
+ const out = new Set();
+ for (const id of candidates) if (!rejected.has(id) && declarations.get(id) === 1) out.add(id);
+ return out;
+}
+
/** Locals living in boxes: declared boxed or captured from an enclosing
* function. */
function boxedLocals(fn: IrFunction): Set {
diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts
index d0593313f..2f69dd5ec 100644
--- a/packages/compiler/src/backend/llvm/walkers.ts
+++ b/packages/compiler/src/backend/llvm/walkers.ts
@@ -64,9 +64,6 @@ export class LlWalkers {
private get S(): "i32" | "i64" {
return this.host.sizeType;
}
- private abiOffset(native64: number, wasm32: number): number {
- return this.S === "i32" ? wasm32 : native64;
- }
/** The value-parameter LLVM type of a writer for `t`. */
private valTy(t: IrType): string {
@@ -220,10 +217,10 @@ export class LlWalkers {
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`);
break;
case "dyn":
- // Overflow values under an `unknown` index signature: the checked-dynamic tree
- // serializes itself (runtime walker). Bare stringify of dyn stays
- // frontend-fenced; this writer is reachable only through overflow
- // entries.
+ // A checked-dynamic value serializes itself (runtime walker).
+ // Bare stringify of dyn stays frontend-fenced, and overflow
+ // entries under an `unknown` index signature go through
+ // scr_jb_put_dyn_entry, which also runs the value's toJSON.
this.host.declare(`declare void @scr_jb_put_dyn(ptr, ptr)`);
B.line(`call void @scr_jb_put_dyn(ptr %b, ptr %v)`);
break;
@@ -324,7 +321,11 @@ export class LlWalkers {
);
}
const byName = new Map(shape.fields.map((f) => [f.name, f]));
- const emitFields = order.map((n) => byName.get(n)).filter((f) => f !== undefined);
+ // Function-valued properties are omitted, as JSON.stringify omits them.
+ const emitFields = order
+ .map((n) => byName.get(n))
+ .filter((f) => f !== undefined)
+ .filter((f) => f.type.kind !== "func");
const droppable =
emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) ||
!!shape.indexValue;
@@ -451,7 +452,8 @@ export class LlWalkers {
order.forEach((fieldName, i) => {
B.startBlock(labels[i]!);
const f = byName.get(fieldName)!;
- if (fieldName.startsWith("%")) {
+ // Internal fields stay hidden; function-valued properties are omitted.
+ if (fieldName.startsWith("%") || f.type.kind === "func") {
B.terminate("ret i1 false");
return;
}
@@ -491,10 +493,13 @@ export class LlWalkers {
}
/** Whether the walker for `t` can leave an exception pending: cyclic
- * values, and records serialized through a kept checked source. */
+ * values, records serialized through a kept checked source, and
+ * checked-dynamic values (toJSON methods, getters, BigInts). */
jsonWriteMayThrow(t: IrType, seen = new Set()): boolean {
if (traceAdapter(this.host, t) !== null) return true;
switch (t.kind) {
+ case "dyn":
+ return true;
case "array":
return this.jsonWriteMayThrow(t.elem, seen);
case "union":
@@ -534,6 +539,15 @@ export class LlWalkers {
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + this.host.nullableUnions.fieldBase(shape.id)}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
+ if (iv.kind === "dyn") {
+ // The runtime writes every entry with JSON.stringify's member rules:
+ // each value's toJSON runs with its key before the entry is dropped
+ // or written, and containers below it join the circular-structure
+ // stack. It may leave an exception pending.
+ host.declare(`declare void @scr_jb_put_dyn_entries(ptr, ptr, ptr, ptr)`);
+ B.line(`call void @scr_jb_put_dyn_entries(ptr %b, ptr %v, ptr ${ovf}, ptr ${first})`);
+ return;
+ }
host.declare(`declare ptr @scr_map_keys_js_order(ptr)`);
host.declare(`declare double @scr_arr_len(ptr)`);
host.declare(`declare ptr @scr_arr_get_ref(ptr, double)`);
@@ -573,23 +587,9 @@ export class LlWalkers {
B.line(`${val} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${k}) ; value (+1)`);
}
// Undefined-valued entries drop, exactly the optional-field rule: a
- // dyn value whose dyn kind follows its size_t RC word (enum
- // member 6 — scr_runtime.h's ScrDynKind), or a union holding its
- // undefined arm.
+ // union holding its undefined arm.
let skipUndef: string | null = null;
- if (iv.kind === "dyn") {
- const kp = B.tmp();
- const kd = B.tmp();
- const isu = B.tmp();
- B.line(
- `${kp} = getelementptr inbounds i8, ptr ${val}, i64 ${this.abiOffset(8, 4)} ; ->kind`,
- );
- B.line(`${kd} = load i32, ptr ${kp}`);
- B.line(
- `${isu} = icmp eq i32 ${kd}, 6 ; SCR_DYN_UNDEF (NULL is 0 — null members DO serialize)`,
- );
- skipUndef = isu;
- } else if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) {
+ if (iv.kind === "union" && undefinedArmTag(iv, this.host.unionsById) >= 0) {
const tag = this.unionTag(B, val, iv.unionId);
const isu = B.tmp();
B.line(`${isu} = icmp eq i32 ${tag}, ${undefinedArmTag(iv, this.host.unionsById)}`);
diff --git a/packages/compiler/src/backend/llvm/wrapping-sums.ts b/packages/compiler/src/backend/llvm/wrapping-sums.ts
index ab61da938..b00b61cbb 100644
--- a/packages/compiler/src/backend/llvm/wrapping-sums.ts
+++ b/packages/compiler/src/backend/llvm/wrapping-sums.ts
@@ -18,9 +18,10 @@ export function markWrappingOperands(host: LlvmEmitterContext, ...operands: IrEx
/** The values an f64 expression can have when each one is an exact integer
* (never -0) or NaN: proven ranges, safe integer literals, NaN-coded reads
- * of integer typed-array elements, `charCodeAt`, and sums and differences of those whose
- * every integer result is still exact in a double. */
-function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerRange | null {
+ * of integer typed-array elements and of Int32[] elements, `charCodeAt`,
+ * and sums and differences of those whose every integer result is still
+ * exact in a double. */
+export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerRange | null {
if (e.type.kind !== "f64") return null;
const range = host.integerRanges.get(e);
if (range) return range;
@@ -35,6 +36,15 @@ function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerRange |
e.receiver.type.kind === "bytes"
)
return ELEMENT_RANGES[e.receiver.type.elem] ?? null;
+ // ToNumber of an Int32[] element read: the stored int32, or NaN for a
+ // hole, a present undefined or a missing index.
+ if (
+ e.kind === "arrIntrinsic" &&
+ e.method === "getNumber" &&
+ host.int32Elements !== null &&
+ host.int32Elements(e.receiver)
+ )
+ return ELEMENT_RANGES.i32!;
// A UTF-16 code unit, or NaN past either end.
if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 };
if (e.kind === "bin" && (e.op === "+" || e.op === "-")) {
diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts
index 1934cc6da..0bc428964 100644
--- a/packages/compiler/src/backend/mangle.ts
+++ b/packages/compiler/src/backend/mangle.ts
@@ -182,6 +182,12 @@ export function mangleAsyncAwaitCall(fnName: string): string {
export function mangleAsyncAdoptCall(fnName: string): string {
return `sc_ad_${sanitize(fnName)}`;
}
+/** A stackless coroutine body's `await f(...)` of a fiberless async f: the
+ * body run in place, returning its result with any throw left pending; the
+ * coroutine takes the await's turn itself. */
+export function mangleAsyncInlineAwaitCall(fnName: string): string {
+ return `sc_ai_${sanitize(fnName)}`;
+}
export function mangleTrampoline(fnName: string): string {
return `sc_tr_${sanitize(fnName)}`;
}
diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts
index 68465a792..039ecc209 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -93,6 +93,7 @@ export class RuntimePackError extends Error {
constructor(
message: string,
readonly code: "missing" | "invalid" | "unsupported",
+ readonly hint?: string,
) {
super(message);
this.name = "RuntimePackError";
diff --git a/packages/compiler/src/backend/runtime-pack-native.test.ts b/packages/compiler/src/backend/runtime-pack-native.test.ts
new file mode 100644
index 000000000..20214818f
--- /dev/null
+++ b/packages/compiler/src/backend/runtime-pack-native.test.ts
@@ -0,0 +1,47 @@
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, expect, test } from "vitest";
+import { nativeCodegenDiag } from "../diagnostics/diagnostic.js";
+import type { NativeLinkFeatures } from "./native-link-info.js";
+import { RuntimePackError } from "./runtime-pack-core.js";
+import { selectNativeRuntimePack } from "./runtime-pack-native.js";
+import { WASM32_WASI_TARGET } from "./targets.js";
+
+const dirs: string[] = [];
+afterEach(async () =>
+ Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))),
+);
+
+test("a missing cross-target runtime pack names its package in the diagnostic", async () => {
+ const root = await mkdtemp(join(tmpdir(), "scriptc-runtime-pack-native-"));
+ dirs.push(root);
+ let error: unknown;
+ try {
+ selectNativeRuntimePack(
+ join(root, "runtime-wasm32-wasi"),
+ WASM32_WASI_TARGET,
+ "0.3.5",
+ {} as NativeLinkFeatures,
+ "release",
+ );
+ } catch (caught) {
+ error = caught;
+ }
+ expect(error).toBeInstanceOf(RuntimePackError);
+ const pack = error as RuntimePackError;
+ expect(pack.code).toBe("missing");
+ const diagnostic = nativeCodegenDiag("SC3003", pack.message, "main.ts", pack.hint);
+ expect(diagnostic.message).toBe(
+ "runtime pack for wasm32-wasi is not installed; install @scriptc/runtime-wasm32-wasi@0.3.5 in your project or set SCRIPTC_RUNTIME_PACK to its directory",
+ );
+ expect(diagnostic.hint).toBe(
+ "scriptc includes the runtime pack for its own platform only; other targets need their runtime pack package in the project",
+ );
+});
+
+test("a missing native helper keeps the reinstall hint", () => {
+ expect(nativeCodegenDiag("SC3003", "helper missing", "main.ts").hint).toBe(
+ "reinstall scriptc with optional dependencies enabled for this host",
+ );
+});
diff --git a/packages/compiler/src/backend/runtime-pack-native.ts b/packages/compiler/src/backend/runtime-pack-native.ts
index cbea1ec67..bd38e2ef0 100644
--- a/packages/compiler/src/backend/runtime-pack-native.ts
+++ b/packages/compiler/src/backend/runtime-pack-native.ts
@@ -53,6 +53,7 @@ export function selectNativeRuntimePack(
throw new RuntimePackError(
`runtime pack for ${target.name} is not installed; install ${target.runtimePackPackage}@${compilerVersion} in your project or set SCRIPTC_RUNTIME_PACK to its directory`,
"missing",
+ "scriptc includes the runtime pack for its own platform only; other targets need their runtime pack package in the project",
);
}
const packageText = readFileSync(join(root, "package.json"), "utf8");
diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts
index 65d954fb5..365d5e913 100644
--- a/packages/compiler/src/backend/target-diagnostics.ts
+++ b/packages/compiler/src/backend/target-diagnostics.ts
@@ -129,6 +129,7 @@ const WASI_UNAVAILABLE_HOST_FACTS: ReadonlyMap = new Map([
["os.loadavg", "system load averages (WASI Preview 1 has no load API)"],
["os.uptime", "system uptime (WASI Preview 1 has no boot-time API)"],
["os.getPriority", "process scheduling priority (WASI Preview 1 has no priority API)"],
+ ["os.setPriority", "process scheduling priority (WASI Preview 1 has no priority API)"],
["process.memoryUsageTake", "process memory statistics (WASI Preview 1 has no memory API)"],
["process.rss", "process memory statistics (WASI Preview 1 has no memory API)"],
["process.ppid", "the parent process id (WASI Preview 1 has no process API)"],
diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts
index 21fb9f097..3c25c9b1b 100644
--- a/packages/compiler/src/contracts/check.ts
+++ b/packages/compiler/src/contracts/check.ts
@@ -11,6 +11,9 @@
* proven by the same integer range analysis the int32 storage uses,
* assuming every Int32 slot holds an int32 (sound by induction: every
* write is checked under that assumption).
+ * - Int32[]: every element stored into an array that can reach the slot is
+ * an int32 (contracts/int32-elements.ts), so present element reads
+ * through slots only such arrays reach are int32s.
* - Acyclic: the class or object type cannot lie on a reference cycle, by
* the cycle analysis that decides tracing (backend/cycle-analysis.ts), or
* the diagnostic shows the cycle.
@@ -57,6 +60,7 @@ import {
type CycleStep,
} from "../backend/cycle-analysis.js";
import { SourceLocations } from "../backend/source-locations.js";
+import { Int32ElementContracts } from "./int32-elements.js";
import {
contractLocKey,
type ContractAnnotations,
@@ -251,7 +255,9 @@ class ContractChecker {
this.checkProgramLifetime();
this.checkNoThrow();
this.checkAcyclic();
- const int32 = new Int32Contracts(this);
+ const elements = new Int32ElementContracts(this);
+ elements.analyze();
+ const int32 = new Int32Contracts(this, elements);
int32.run();
const immutable = new ImmutableContracts(this);
immutable.run();
@@ -604,19 +610,24 @@ class Int32Contracts {
private readonly returns = new Map(); // fn
private readonly locals = new Map(); // fn\0localId or \0globalId
- constructor(private readonly c: ContractChecker) {
+ constructor(
+ private readonly c: ContractChecker,
+ private readonly elements: Int32ElementContracts,
+ ) {
this.slots = new Int32Slots(c.classes);
}
run(): void {
if (!this.bind()) return;
- if (this.fields.size + this.params.size + this.returns.size + this.locals.size === 0) return;
+ const scalars = this.fields.size + this.params.size + this.returns.size + this.locals.size;
+ if (scalars === 0 && !this.elements.active) return;
this.structural();
this.writes();
this.c.record("int32Fields", [...this.fields.keys()]);
this.c.record("int32Params", [...this.params.keys()]);
this.c.record("int32Returns", [...this.returns.keys()]);
this.c.record("int32Locals", [...this.locals.keys()]);
+ this.c.record("int32Elements", [...this.elements.int32Slots]);
}
private refuse(note: ContractNote, why: string, hint: string): void {
@@ -632,7 +643,7 @@ class Int32Contracts {
const c = this.c;
for (const [key, list] of c.notes.functions)
for (const note of list) {
- if (note.marker !== "Int32") continue;
+ if (note.marker !== "Int32" || note.elements === true) continue;
for (const fn of c.functionsAt(key)) {
if (fn.async === true || fn.generator !== undefined) {
this.refuse(
@@ -654,7 +665,7 @@ class Int32Contracts {
}
}
for (const [key, note] of c.notes.bindings) {
- if (note.marker !== "Int32") continue;
+ if (note.marker !== "Int32" || note.elements === true) continue;
for (const b of c.bindingsOf(key)) {
const type = b.fn
? b.fn.locals.find((l) => l.id === b.id)?.type
@@ -677,7 +688,7 @@ class Int32Contracts {
}
}
for (const [key, note] of c.notes.fields) {
- if (note.marker !== "Int32") continue;
+ if (note.marker !== "Int32" || note.elements === true) continue;
const cut = key.lastIndexOf("\0");
const field = key.slice(cut + 1);
for (const cls of c.classesAt(key.slice(0, cut))) {
@@ -821,6 +832,7 @@ class Int32Contracts {
this.locals.has(`${fnName}\0${localId}`) || this.locals.has(`\0${localId}`)
? INT32_RANGE
: null,
+ elements: (arr) => this.elements.readsInt32(fnName, arr),
};
}
@@ -841,9 +853,14 @@ class Int32Contracts {
: "make the int32 conversion explicit with '| 0', so Node computes the same value",
);
};
+ const elementFunctions = this.elements.functionsWithWrites();
+ const elementRanges = new Map();
for (const fn of c.mod.functions) {
const numeric = withInitializerBindings(fn, bindings.get(fn.name) ?? []);
const ranges = analyzeIntegerRanges(numeric, this.int32Facts(fn.name));
+ if (elementFunctions.has(fn.name)) elementRanges.set(fn.name, ranges);
+ // Scalar Int32 slots accept exactly what the backend's int32 storage
+ // proves (int32-slots.ts stores pinned slots from proven writes).
const int32 = (e: IrExpr): boolean => e.type.kind === "f64" && withinInt32(ranges.get(e));
const localNote = (id: string): ContractNote | undefined =>
this.locals.get(`${fn.name}\0${id}`) ?? this.locals.get(`\0${id}`);
@@ -930,6 +947,11 @@ class Int32Contracts {
},
});
}
+ this.elements.checkWrites((fn, value) => {
+ const proven = (e: IrExpr): boolean =>
+ e.type.kind === "f64" && withinInt32(elementRanges.get(fn.name)?.get(e));
+ return proven(value) || this.elements.provenElement(fn, value, proven);
+ });
}
}
diff --git a/packages/compiler/src/contracts/int32-elements.ts b/packages/compiler/src/contracts/int32-elements.ts
new file mode 100644
index 000000000..8284d40ab
--- /dev/null
+++ b/packages/compiler/src/contracts/int32-elements.ts
@@ -0,0 +1,1271 @@
+/* Int32[] contracts: every present element of an array that can reach an
+ * `Int32[]` slot is an int32.
+ *
+ * The contract is about arrays, not slots: whoever writes an array that is
+ * (or later becomes) the value of an `Int32[]` slot must store int32s,
+ * through any reference to it. The check therefore works on the flow of
+ * array references through the whole program. Nodes are the places an
+ * array can live, keyed by IR names like int32-slots.ts keys:
+ *
+ * l:\0 locals and parameters (captured bindings link both ways)
+ * g: module globals
+ * r: function results
+ * f: class field families
+ * s:\0 record fields (`*`: the index signature's entries)
+ * e:/mk:/mv:/se: elements, map keys and values, set
+ * elements of every container of that type
+ *
+ * plus one node per allocation (an array literal, a length-only array) and
+ * one per opaque origin (a caller scriptc cannot see, dynamic code, a
+ * library result). Every IR store adds an edge from the stored value's
+ * nodes to the slot; an operation that copies elements from one array into
+ * another (spreads, `concat`, `slice`, splice items) links their nodes as
+ * if both were the same array, which over-approximates both directions.
+ *
+ * - The allocations that can reach an annotated slot are constrained: every
+ * element store into an array from one of them, wherever it happens, must
+ * be an int32 by construction (the same integer range proofs as Int32,
+ * with the elements of other Int32[] arrays assumed int32: sound by
+ * induction). An opaque origin that can reach an annotated slot refuses
+ * the contract (SC7003): its arrays were filled by code scriptc does not
+ * check.
+ * - A slot that can hold a constrained array is checked; such a value may
+ * not flow anywhere the IR does not show (dynamic code, function values,
+ * library calls that are not read-only, throws, promises) (SC7003).
+ * - A slot that no unconstrained allocation and no opaque origin can reach
+ * holds only constrained arrays: an element read through it that finds a
+ * value reads an int32. Those slots are recorded as
+ * IrContracts.int32Elements.
+ *
+ * Holes, present undefined, out-of-range reads and `length` changes keep
+ * their JavaScript behavior: the contract constrains the values stored, not
+ * the array's shape. */
+import type { ContractNote, ContractAnnotations } from "../frontend/contract-types.js";
+import type {
+ IrClassDef,
+ IrExpr,
+ IrFunction,
+ IrModule,
+ IrRecordShape,
+ IrStmt,
+ IrType,
+ SrcLoc,
+} from "../ir/ir.js";
+import { typeKey } from "../ir/ir.js";
+import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
+import { functionCallFacts, int32ElementSlot, Int32Slots } from "../ir/int32-slots.js";
+
+/** What the Int32[] check needs from the contract checker. */
+export interface ElementContractHost {
+ readonly mod: IrModule;
+ readonly notes: ContractAnnotations;
+ readonly functions: ReadonlyMap;
+ readonly classes: ReadonlyMap;
+ report(code: `SC${number}`, loc: SrcLoc, message: string, hint: string): void;
+ text(loc: SrcLoc, max?: number): string;
+ where(loc: SrcLoc | null): string;
+ displayName(fnName: string): string;
+ bindingsOf(key: string): { fn: IrFunction | null; id: string }[];
+ functionsAt(key: string): readonly IrFunction[];
+ classesAt(key: string): readonly IrClassDef[];
+}
+
+/** An element store into a number array, checked once ranges are known. */
+interface ElementWrite {
+ fn: IrFunction;
+ /** The nodes of the array written. */
+ target: string[];
+ /** The stored number. */
+ value: IrExpr;
+ /** Source text of the written array, for messages. */
+ arrayText: string;
+ /** An element of an array literal. */
+ literal?: true;
+ /** The written array expression (element stores and methods). */
+ arr?: IrExpr;
+}
+
+/** A value leaving the IR's sight, with the nodes it can hold. */
+interface Escape {
+ nodes: string[];
+ loc: SrcLoc;
+ shown: string;
+ what: string;
+}
+
+/** Array methods that store their numeric arguments as elements, by the
+ * argument positions they store. */
+const ELEMENT_ARGS: ReadonlyMap = new Map([
+ ["push", "all"],
+ ["unshift", "all"],
+ ["fill", 0],
+ ["with", 1],
+]);
+
+const NO_NODES: ReadonlySet = new Set();
+
+/** Array methods whose result is a new array holding copies of the
+ * receiver's (and array arguments') elements. */
+const COPIES = new Set([
+ "slice",
+ "splice",
+ "concatSpread",
+ "toSpliced",
+ "toReversed",
+ "toSortedPrimitive",
+ "with",
+ "withUndefined",
+]);
+
+/** Array methods that copy the elements of an array argument into the
+ * receiver. */
+const COPY_IN = new Set(["pushSpread", "unshiftSpread", "spliceInsert"]);
+
+/** Library calls that only read array arguments. */
+const READ_ONLY_LIB = /^(console\.|inspect\.|json\.stringify|math\.|insp\.)/;
+
+/** Library calls that always throw (the value they stand for never exists). */
+const THROWING_LIB = /^error\.\w*Throw$/;
+
+/** Expression kinds whose array result is one of their operands. */
+const PASS_THROUGH = new Set([
+ "upcast",
+ "downcast",
+ "unionWrap",
+ "unionRetype",
+ "unionNarrow",
+ "ternary",
+ "logical",
+ "nullish",
+ "orDefault",
+ "seqExpr",
+ "chainRecv",
+ "optChain",
+ "assignExpr",
+]);
+
+/** Kinds that only inspect an array operand (identity, truthiness, text). */
+const OBSERVE = new Set([
+ "toBool",
+ "strEq",
+ "unionEq",
+ "unionFuncEq",
+ "unionIsTag",
+ "unionDisc",
+ "instanceOf",
+ "instanceOfValue",
+ "dynScalarEq",
+ "fieldAbsent",
+ "recordHas",
+ "recordOvfHas",
+ "recordNullProto",
+ "arrayHas",
+ "arrayState",
+ "jsonStringify",
+ "toString",
+ "strConcat",
+ "bin",
+ "unary",
+ "unionKeyGet",
+ "recordKeyGet",
+ "recordOvfKeys",
+ "fieldGet",
+ "recordGet",
+ "arrayGet",
+]);
+
+export class Int32ElementContracts {
+ /** Annotated slots and their notes. */
+ private readonly annotated = new Map();
+ private readonly succ = new Map>();
+ private readonly pred = new Map>();
+ /** Element copies: the nodes of the arrays whose elements are copied,
+ * and the nodes of the array they are copied into. A copy holds none of
+ * its source's references, but the source of a copy into a constrained
+ * array must be constrained too. */
+ private readonly copies: { from: string[]; to: string[] }[] = [];
+ /** Allocation nodes (`a:`) and opaque origins (`o:`). */
+ private readonly allocs = new Set();
+ private readonly opaque = new Map();
+ private readonly writes: ElementWrite[] = [];
+ private readonly escapes: Escape[] = [];
+ private readonly slots: Int32Slots;
+ private nextNode = 0;
+ /** Nodes that may hold a constrained array, with the note it reaches. */
+ private readonly constrainedReach = new Map();
+ /** Nodes that may hold an unconstrained or opaque array. */
+ private readonly unconstrainedReach = new Set();
+ private readonly containedMemo = new Map();
+ private readonly unions: ReadonlyMap;
+ private readonly records: ReadonlyMap;
+ private readonly methodImpls = new Map();
+ private readonly subclasses = new Map();
+ /** Functions whose callers are not all visible. */
+ private valueEscaped: ReadonlySet = new Set();
+ private runtimeVirtual: (name: string) => boolean = () => false;
+
+ constructor(private readonly c: ElementContractHost) {
+ this.slots = new Int32Slots(c.classes);
+ this.unions = new Map((c.mod.unions ?? []).map((u) => [u.id, u.arms]));
+ this.records = new Map((c.mod.records ?? []).map((r) => [r.id, r]));
+ for (const cls of c.classes.values()) {
+ for (const m of cls.methods ?? []) push(this.methodImpls, m, `%${cls.name}.${m}`);
+ if (cls.base) push(this.subclasses, cls.base, cls.name);
+ }
+ }
+
+ /** Whether any Int32[] annotation exists. */
+ get active(): boolean {
+ return this.annotated.size > 0;
+ }
+
+ /** Bind the annotations, build the flow graph and compute which slots
+ * hold constrained arrays. Reports structural refusals. */
+ analyze(): void {
+ this.bind();
+ if (this.annotated.size === 0) return;
+ const calls = functionCallFacts(this.c.mod, this.c.classes, (name) => this.subtree(name));
+ this.valueEscaped = calls.valueEscaped;
+ this.runtimeVirtual = calls.runtimeVirtual;
+ for (const fn of this.c.mod.functions) this.scanFunction(fn);
+ this.solve();
+ }
+
+ /** Whether reads of `arr` inside `fnName` find only int32 elements. */
+ readsInt32(fnName: string, arr: IrExpr): boolean {
+ const key = int32ElementSlot(fnName, arr, (cls, field) => this.slots.family(cls, field));
+ return key !== null && this.int32Slots.has(key);
+ }
+
+ /** Slots recorded as IrContracts.int32Elements. */
+ readonly int32Slots = new Set();
+
+ private readonly readers = new Map();
+ private readonly localWrites = new Map>();
+
+ /** Whether the number `e` is an element an Int32[] read found: the number
+ * arm of an ordinary `xs[i]` read (the lowering's element-read helper
+ * returns `number | undefined`, and the read narrows it, throwing for
+ * undefined), directly or through a local holding such reads. `int32`
+ * answers the range proof for any other number. */
+ provenElement(fn: IrFunction, e: IrExpr, int32: (e: IrExpr) => boolean): boolean {
+ if (this.int32Slots.size === 0) return false;
+ const value = (x: IrExpr, depth: number): boolean => {
+ if (int32(x)) return true;
+ if (depth > 8) return false;
+ if (x.kind === "ternary") return arm(x.then, depth + 1) && arm(x.else_, depth + 1);
+ if (x.kind === "unionNarrow") return holder(x.value, depth + 1);
+ return false;
+ };
+ const arm = (x: IrExpr, depth: number): boolean =>
+ (x.kind === "libCall" && THROWING_LIB.test(x.fn)) || value(x, depth);
+ const holder = (x: IrExpr, depth: number): boolean => {
+ if (depth > 8) return false;
+ switch (x.kind) {
+ case "call": {
+ const k = this.elementReader(x.callee);
+ const arr = k === null ? undefined : x.args[k];
+ return arr !== undefined && this.readsInt32(fn.name, arr);
+ }
+ case "varRef": {
+ const writes = this.writesOf(fn).get(x.localId);
+ return (
+ writes !== undefined &&
+ writes !== null &&
+ writes.length > 0 &&
+ writes.every((w) => holder(w, depth + 1))
+ );
+ }
+ case "unionWrap":
+ return x.value.kind === "unitLit" || value(x.value, depth + 1);
+ case "unitLit":
+ return true;
+ case "ternary":
+ return holder(x.then, depth + 1) && holder(x.else_, depth + 1);
+ default:
+ return false;
+ }
+ };
+ return value(e, 0);
+ }
+
+ /** Whether `value` is an element of the written array itself, or of a
+ * copy of it the function took (the lowered `sort` moves elements
+ * through such a snapshot): storing it adds no new value to the array,
+ * whatever its contents. Both must be locals the function never
+ * reassigns. */
+ private movedWithin(fn: IrFunction, arr: IrExpr, value: IrExpr): boolean {
+ if (arr.kind !== "varRef" || value.kind !== "arrayGet" || value.arr.kind !== "varRef")
+ return false;
+ const writes = this.writesOf(fn);
+ const target = arr.localId;
+ const source = value.arr.localId;
+ const fixed = (id: string): boolean => {
+ const list = writes.get(id);
+ if (list === null || !fn.locals.some((l) => l.id === id)) return false;
+ const isParam = fn.params.some((p) => p.localId === id);
+ return isParam ? list === undefined : list !== undefined && list.length === 1;
+ };
+ if (!fixed(target)) return false;
+ if (source === target) return true;
+ if (!fixed(source)) return false;
+ const init = writes.get(source)?.[0];
+ return (
+ init !== undefined &&
+ init.kind === "arrIntrinsic" &&
+ init.method === "slice" &&
+ init.receiver.kind === "varRef" &&
+ init.receiver.localId === target &&
+ this.onlyMoves(fn, source, target, init)
+ );
+ }
+
+ /** Whether the function uses the snapshot local `source` only to read,
+ * reorder and move its own elements (or the target's): then every value
+ * it holds is one of the target's elements. */
+ private onlyMoves(fn: IrFunction, source: string, target: string, init: IrExpr): boolean {
+ const allowed = new Set();
+ const isRef = (e: IrExpr, id: string): boolean => e.kind === "varRef" && e.localId === id;
+ const moved = (v: IrExpr): boolean =>
+ v.kind === "arrayGet" && (isRef(v.arr, source) || isRef(v.arr, target));
+ let ok = true;
+ const expr = (e: IrExpr): boolean => {
+ if (
+ (e.kind === "arrayGet" || e.kind === "arrayState" || e.kind === "arrayHas") &&
+ isRef(e.arr, source)
+ )
+ allowed.add(e.arr);
+ else if (
+ e.kind === "arrIntrinsic" &&
+ isRef(e.receiver, source) &&
+ ["length", "nextPresent", "sortValues", "sortDense", "reverse", "copyWithin"].includes(
+ e.method,
+ )
+ )
+ allowed.add(e.receiver);
+ else if (e.kind === "varRef" && e.localId === source && !allowed.has(e)) ok = false;
+ return ok && everyExprChild(e, expr, stmt);
+ };
+ const stmt = (st: IrStmt): boolean => {
+ if (
+ (st.kind === "arraySet" && isRef(st.arr, source) && moved(st.value)) ||
+ ((st.kind === "arraySetUndefined" || st.kind === "arraySetLength") && isRef(st.arr, source))
+ )
+ allowed.add(st.arr);
+ else if (st.kind === "varDecl" && st.localId === source && st.init !== init) ok = false;
+ return ok && everyStmtChild(st, expr, stmt);
+ };
+ for (const st of fn.body) if (!stmt(st)) break;
+ return ok;
+ }
+
+ /** The parameter whose element every return of `fnName` reads (or
+ * undefined): the lowering's `xs[i]` helper. Null for other functions. */
+ private elementReader(fnName: string): number | null {
+ const known = this.readers.get(fnName);
+ if (known !== undefined) return known;
+ this.readers.set(fnName, null);
+ const fn = this.c.functions.get(fnName);
+ if (!fn || fn.captures?.length || fn.async || fn.generator) return null;
+ let result: number | null = null;
+ for (let k = 0; k < fn.params.length && result === null; k++) {
+ const id = fn.params[k]!.localId;
+ if (this.writesOf(fn).get(id) !== undefined) continue;
+ const element = (x: IrExpr): boolean => {
+ switch (x.kind) {
+ case "ternary":
+ return element(x.then) && element(x.else_);
+ case "arrayGet":
+ return x.arr.kind === "varRef" && x.arr.localId === id;
+ case "unitLit":
+ return true;
+ case "unionWrap":
+ case "upcast":
+ return element(x.value);
+ default:
+ return false;
+ }
+ };
+ let returns = 0;
+ let ok = true;
+ const visit = (st: IrStmt): boolean => {
+ if (st.kind === "return") {
+ returns++;
+ if (!st.value || !element(st.value)) ok = false;
+ }
+ return everyStmtChild(st, () => true, visit);
+ };
+ for (const st of fn.body) visit(st);
+ if (ok && returns > 0) result = k;
+ }
+ this.readers.set(fnName, result);
+ return result;
+ }
+
+ /** Every value written to each local of `fn` (null: a write whose value
+ * is not an expression, such as `++` or a for-of binding). */
+ private writesOf(fn: IrFunction): Map {
+ const known = this.localWrites.get(fn.name);
+ if (known) return known;
+ const out = new Map();
+ const add = (id: string, value: IrExpr | null): void => {
+ const list = out.get(id);
+ if (list === null) return;
+ if (value === null) out.set(id, null);
+ else if (list) list.push(value);
+ else out.set(id, [value]);
+ };
+ for (const l of fn.locals) if (l.boxed) out.set(l.id, null);
+ for (const cap of [...(fn.captures ?? []), ...(fn.classCaptures ?? [])])
+ out.set(cap.localId, null);
+ const expr = (e: IrExpr): boolean => {
+ if (e.kind === "assignExpr") add(e.localId, e.value);
+ else if (e.kind === "incDec") add(e.localId, null);
+ return everyExprChild(e, expr, stmt);
+ };
+ const stmt = (st: IrStmt): boolean => {
+ if (st.kind === "varDecl" && st.init) add(st.localId, st.init);
+ else if (st.kind === "assign") add(st.localId, st.value);
+ else if (st.kind === "forOf") add(st.localId, null);
+ else if (st.kind === "tryCatch" && st.catchLocalId !== null) add(st.catchLocalId, null);
+ return everyStmtChild(st, expr, stmt);
+ };
+ for (const st of fn.body) stmt(st);
+ this.localWrites.set(fn.name, out);
+ return out;
+ }
+
+ /** Check every element store into a constrained array, given an int32
+ * test for the stored value inside its function. */
+ checkWrites(int32: (fn: IrFunction, value: IrExpr) => boolean): void {
+ for (const w of this.writes) {
+ const note = this.reachingNote(w.target);
+ if (note === undefined || int32(w.fn, w.value)) continue;
+ if (w.arr !== undefined && this.movedWithin(w.fn, w.arr, w.value)) continue;
+ const at = this.displayed(w.fn, w.value);
+ const shown = this.c.text(at);
+ // A store inside a lowered array method (`map`, `from`) carries the
+ // method call's location for both the array and the value.
+ const builtIn = shown !== "" && shown === w.arrayText;
+ const what = w.literal
+ ? `the array literal element${shown ? ` '${shown}'` : ""} is not provably an int32`
+ : builtIn
+ ? `the elements '${shown}' stores are not provably int32s`
+ : `the element stored into ${w.arrayText ? `'${w.arrayText}'` : "an array"} is not provably an int32${shown ? `: '${shown}'` : ""}`;
+ this.c.report(
+ "SC7002",
+ at,
+ `${what} (the array can reach '${note.name}', declared Int32[])`,
+ builtIn
+ ? "callback results are not followed; build the array with a loop that stores 'value | 0'"
+ : shown
+ ? `make the int32 conversion explicit, for example '${orZero(shown)}', so Node stores the same value`
+ : "make the int32 conversion explicit with '| 0', so Node stores the same value",
+ );
+ }
+ }
+
+ /** The source location that shows a stored value. An element read
+ * stored as is (`a[i] = b[j]`) lowers to a narrowing of a temporary that
+ * carries the whole statement's location; show the read instead. */
+ private displayed(fn: IrFunction, value: IrExpr): SrcLoc {
+ let e = value;
+ for (let depth = 0; depth < 4; depth++) {
+ if (e.kind === "ternary") e = e.then;
+ else if (e.kind === "unionNarrow") e = e.value;
+ else if (e.kind === "varRef") {
+ const writes = this.writesOf(fn).get(e.localId);
+ if (!writes || writes.length !== 1) break;
+ e = writes[0]!;
+ } else break;
+ if (e.loc.start > value.loc.start || e.loc.end < value.loc.end) return e.loc;
+ }
+ return value.loc;
+ }
+
+ /** Functions with element stores to check. */
+ functionsWithWrites(): Set {
+ const out = new Set();
+ for (const w of this.writes) if (this.reachingNote(w.target) !== undefined) out.add(w.fn.name);
+ return out;
+ }
+
+ /* ── binding ── */
+
+ private bind(): void {
+ this.bindAnnotations();
+ // Messages name the first annotation an array reaches: prefer module
+ // bindings and fields over parameters and results.
+ const rank = (slot: string): number =>
+ slot.startsWith("g:") ? 0 : slot.startsWith("f:") ? 1 : slot.startsWith("l:") ? 2 : 3;
+ const ordered = [...this.annotated].sort((a, b) => rank(a[0]) - rank(b[0]));
+ this.annotated.clear();
+ for (const [slot, note] of ordered) this.annotated.set(slot, note);
+ }
+
+ private bindAnnotations(): void {
+ const c = this.c;
+ const refuse = (note: ContractNote, why: string, hint: string): void =>
+ c.report(
+ "SC7003",
+ note.loc,
+ `the Int32[] contract on '${note.name}' cannot be checked: ${why}`,
+ hint,
+ );
+ const numberArray = (t: IrType | undefined): boolean =>
+ t !== undefined && t.kind === "array" && t.elem.kind === "f64";
+ for (const [key, list] of c.notes.functions)
+ for (const note of list) {
+ if (note.marker !== "Int32" || note.elements !== true) continue;
+ for (const fn of c.functionsAt(key)) {
+ if (fn.async === true || fn.generator !== undefined) {
+ refuse(
+ note,
+ "async functions and generators return promises or iterators",
+ "annotate the awaited value where it is stored instead",
+ );
+ continue;
+ }
+ if (!numberArray(fn.returnType)) {
+ refuse(note, "the result is not a plain number array", "declare it as Int32[]");
+ continue;
+ }
+ this.annotated.set(`r:${fn.name}`, note);
+ }
+ }
+ for (const [key, note] of c.notes.bindings) {
+ if (note.marker !== "Int32" || note.elements !== true) continue;
+ for (const b of c.bindingsOf(key)) {
+ const type = b.fn
+ ? b.fn.locals.find((l) => l.id === b.id)?.type
+ : c.mod.globals?.find((g) => g.id === b.id)?.type;
+ if (!numberArray(type)) {
+ refuse(
+ note,
+ "the binding is not a plain number array (optional and union types are not checked)",
+ "declare it as a required Int32[]",
+ );
+ continue;
+ }
+ this.annotated.set(b.fn ? `l:${b.fn.name}\0${b.id}` : `g:${b.id}`, note);
+ }
+ }
+ for (const [key, note] of c.notes.fields) {
+ if (note.marker !== "Int32" || note.elements !== true) continue;
+ const cut = key.lastIndexOf("\0");
+ const field = key.slice(cut + 1);
+ for (const cls of c.classesAt(key.slice(0, cut))) {
+ const def = cls.fields.find((f) => f.name === field);
+ const family = this.slots.family(cls.name, field);
+ if (!def || family === null) {
+ refuse(
+ note,
+ `the compiled class has no field '${field}'`,
+ "annotate an ordinary instance field",
+ );
+ continue;
+ }
+ if (!numberArray(def.type)) {
+ refuse(
+ note,
+ "the field is not a plain number array",
+ "declare the field as a required Int32[]",
+ );
+ continue;
+ }
+ this.annotated.set(`f:${family}`, note);
+ }
+ }
+ }
+
+ private subtree(name: string): string[] {
+ const out: string[] = [];
+ const stack = [name];
+ while (stack.length) {
+ const next = stack.pop()!;
+ out.push(next);
+ for (const child of this.subclasses.get(next) ?? []) stack.push(child);
+ }
+ return out;
+ }
+
+ /* ── types ── */
+
+ /** A number array, or a union with a number-array arm: values whose
+ * identity the graph follows. */
+ private direct(t: IrType): boolean {
+ if (t.kind === "array") return t.elem.kind === "f64";
+ if (t.kind === "union")
+ return (this.unions.get(t.unionId) ?? []).some(
+ (a) => a.kind === "array" && a.elem.kind === "f64",
+ );
+ return false;
+ }
+
+ /** The container slots holding number arrays that a value of type `t`
+ * can reach through fields and elements. */
+ private contained(t: IrType): string[] {
+ const key = typeKey(t);
+ const memo = this.containedMemo.get(key);
+ if (memo) return memo;
+ const out = new Set();
+ this.containedMemo.set(key, []);
+ const add = (slot: string, inner: IrType): void => {
+ if (this.direct(inner)) out.add(slot);
+ for (const s of this.contained(inner)) out.add(s);
+ };
+ switch (t.kind) {
+ case "array":
+ add(`e:${key}`, t.elem);
+ break;
+ case "set":
+ add(`se:${key}`, t.elem);
+ break;
+ case "map":
+ add(`mk:${key}`, t.key);
+ add(`mv:${key}`, t.value);
+ break;
+ case "promise":
+ add(`p:${key}`, t.inner);
+ break;
+ case "union":
+ for (const arm of this.unions.get(t.unionId) ?? [])
+ for (const s of this.contained(arm)) out.add(s);
+ break;
+ case "record": {
+ const shape = this.records.get(t.shapeId);
+ for (const f of shape?.fields ?? []) add(`s:${t.shapeId}\0${f.name}`, f.type);
+ if (shape?.indexValue) add(`s:${t.shapeId}\0*`, shape.indexValue);
+ break;
+ }
+ case "object":
+ for (const name of this.subtree(t.className)) {
+ const cls = this.c.classes.get(name);
+ for (const f of cls?.fields ?? []) {
+ const family = this.slots.family(name, f.name);
+ if (family !== null) add(`f:${family}`, f.type);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ const list = [...out];
+ this.containedMemo.set(key, list);
+ return list;
+ }
+
+ private holds(t: IrType): boolean {
+ return this.direct(t) || this.contained(t).length > 0;
+ }
+
+ /* ── graph ── */
+
+ private edge(from: string, to: string): void {
+ if (from === to) return;
+ let s = this.succ.get(from);
+ if (!s) this.succ.set(from, (s = new Set()));
+ s.add(to);
+ let p = this.pred.get(to);
+ if (!p) this.pred.set(to, (p = new Set()));
+ p.add(from);
+ }
+
+ private copy(from: readonly string[], to: readonly string[]): void {
+ if (from.length > 0 && to.length > 0) this.copies.push({ from: [...from], to: [...to] });
+ }
+
+ private newOpaque(loc: SrcLoc, why: string): string {
+ const node = `o:${this.nextNode++}`;
+ this.opaque.set(node, { loc, why });
+ return node;
+ }
+
+ /** An origin the IR does not show produces a value of type `t`. */
+ private opaqueValue(t: IrType, loc: SrcLoc, why: string): string[] {
+ const node = this.newOpaque(loc, why);
+ for (const slot of this.contained(t)) this.edge(node, slot);
+ return this.direct(t) ? [node] : [];
+ }
+
+ private store(sources: readonly string[], slot: string): void {
+ for (const s of sources) this.edge(s, slot);
+ }
+
+ private localSlot(fn: IrFunction, id: string): string {
+ return id.startsWith("%g.") ? `g:${id}` : `l:${fn.name}\0${id}`;
+ }
+
+ private scanFunction(fn: IrFunction): void {
+ const c = this.c;
+ const chains = new Map();
+ const memo = new Map();
+ // Optional-chain receivers, by chain id, before any read of one.
+ const findChains = (e: IrExpr): boolean => {
+ if (e.kind === "optChain") chains.set(e.id, e.receiver);
+ return everyExprChild(e, findChains, findChainsIn);
+ };
+ const findChainsIn = (st: IrStmt): boolean => everyStmtChild(st, findChains, findChainsIn);
+ for (const st of fn.body) findChainsIn(st);
+ // Parameters whose callers are not all visible hold opaque arrays.
+ if (this.valueEscaped.has(fn.name) || this.runtimeVirtual(fn.name))
+ for (const p of fn.params)
+ if (this.holds(p.type)) {
+ const why = this.valueEscaped.has(fn.name)
+ ? `the callers of '${c.displayName(fn.name)}', which is used as a value (its callers are not all visible)`
+ : `the runtime, which calls '${c.displayName(fn.name)}'`;
+ for (const node of this.opaqueValue(p.type, fn.loc, why))
+ this.edge(node, this.localSlot(fn, p.localId));
+ }
+
+ /** The nodes an expression's array value can come from. */
+ const sources = (e: IrExpr): string[] => {
+ if (!this.direct(e.type)) return [];
+ const known = memo.get(e);
+ if (known) return known;
+ memo.set(e, []);
+ const out = sourcesOf(e);
+ memo.set(e, out);
+ return out;
+ };
+ const elementSlot = (container: IrExpr): string[] => {
+ const t = container.type;
+ if (t.kind === "array" && this.direct(t.elem)) return [`e:${typeKey(t)}`];
+ return [];
+ };
+ const sourcesOf = (e: IrExpr): string[] => {
+ switch (e.kind) {
+ case "varRef":
+ return [this.localSlot(fn, e.localId)];
+ case "fieldGet": {
+ const family = this.slots.family(e.className, e.field);
+ return family === null
+ ? this.opaqueValue(e.type, e.loc, "a field scriptc cannot follow")
+ : [`f:${family}`];
+ }
+ case "recordGet":
+ return [`s:${e.shapeId}\0${e.field}`];
+ case "recordKeyGet": {
+ const t = e.obj.type;
+ if (t.kind !== "record") return this.opaqueValue(e.type, e.loc, "a dynamic property");
+ const shape = this.records.get(t.shapeId);
+ const out = [`s:${t.shapeId}\0*`];
+ for (const f of shape?.fields ?? [])
+ if (this.direct(f.type)) out.push(`s:${t.shapeId}\0${f.name}`);
+ return out;
+ }
+ case "arrayGet":
+ return elementSlot(e.arr);
+ case "mapIntrinsic":
+ if (e.method === "get" || e.method === "iterValue")
+ return [`mv:${typeKey(e.receiver.type)}`];
+ if (e.method === "iterKey") return [`mk:${typeKey(e.receiver.type)}`];
+ // Any other result is a stored key or value, or a fresh array.
+ return [
+ `mv:${typeKey(e.receiver.type)}`,
+ `mk:${typeKey(e.receiver.type)}`,
+ ...this.opaqueValue(e.type, e.loc, `Map.${e.method}`),
+ ];
+ case "setIntrinsic":
+ if (e.method === "iterKey") return [`se:${typeKey(e.receiver.type)}`];
+ return [
+ `se:${typeKey(e.receiver.type)}`,
+ ...this.opaqueValue(e.type, e.loc, `Set.${e.method}`),
+ ];
+ case "call":
+ return c.functions.has(e.callee)
+ ? [`r:${e.callee}`]
+ : this.opaqueValue(e.type, e.loc, `a call to '${e.callee}'`);
+ case "virtualCall": {
+ const impls = this.methodImpls.get(e.method) ?? [];
+ return impls.length > 0 ? impls.map((impl) => `r:${impl}`) : unknown(e);
+ }
+ case "arrayLit":
+ case "arrayNewLen":
+ return [alloc()];
+ case "arrIntrinsic": {
+ if (COPIES.has(e.method)) {
+ // A new array of copied elements.
+ const node = alloc();
+ for (const part of [e.receiver, ...e.args])
+ this.copy([...sources(part), ...elementSlot(part)], [node]);
+ return [node];
+ }
+ if (e.method === "flatCopy" || e.method === "flatOne") {
+ // Copies (or flattens) the receiver into the supplied array,
+ // which it returns.
+ const into = e.args.flatMap((a) => sources(a));
+ this.copy([...sources(e.receiver), ...elementSlot(e.receiver)], into);
+ return into;
+ }
+ // The receiver itself (fill, reverse, sort) or one of its elements.
+ return [...sources(e.receiver), ...elementSlot(e.receiver)];
+ }
+ case "upcast":
+ case "downcast":
+ case "unionWrap":
+ case "unionRetype":
+ case "unionNarrow":
+ case "assignExpr":
+ return sources(e.value);
+ case "ternary":
+ return [...sources(e.then), ...sources(e.else_)];
+ case "logical":
+ case "nullish":
+ case "orDefault":
+ return [...sources(e.left), ...sources(e.right)];
+ case "seqExpr":
+ return sources(e.result);
+ case "optChain":
+ return sources(e.body);
+ case "chainRecv": {
+ const receiver = chains.get(e.id);
+ return receiver
+ ? sources(receiver)
+ : this.opaqueValue(e.type, e.loc, "an optional chain");
+ }
+ case "unitLit":
+ return [];
+ default:
+ return unknown(e);
+ }
+ };
+ const alloc = (): string => {
+ const node = `a:${this.nextNode++}`;
+ this.allocs.add(node);
+ return node;
+ };
+ /** An operation scriptc does not follow: a fresh array of unknown
+ * contents, or any array its operands hold or reach. */
+ const unknown = (e: IrExpr): string[] => {
+ const out = this.opaqueValue(e.type, e.loc, describe(e));
+ everyExprChild(
+ e,
+ (child) => {
+ out.push(...sources(child), ...this.contained(child.type));
+ return true;
+ },
+ () => true,
+ );
+ return out;
+ };
+
+ const escape = (value: IrExpr, at: SrcLoc, what: string): void => {
+ if (!this.holds(value.type)) return;
+ const nodes = [...sources(value), ...this.contained(value.type)];
+ if (nodes.length > 0) this.escapes.push({ nodes, loc: at, shown: c.text(value.loc), what });
+ };
+ const write = (arr: IrExpr, value: IrExpr): void => {
+ this.writes.push({ fn, target: sources(arr), value, arrayText: c.text(arr.loc), arr });
+ };
+ const args = (callee: string, list: IrExpr[], offset: number, at: SrcLoc): void => {
+ const target = c.functions.get(callee);
+ list.forEach((arg, i) => {
+ if (!this.holds(arg.type)) return;
+ const param = target?.params[i + offset];
+ if (!target || !param) {
+ escape(arg, at, `a call to '${c.displayName(callee)}'`);
+ return;
+ }
+ this.store(sources(arg), this.localSlot(target, param.localId));
+ });
+ };
+ const isNumberArray = (t: IrType): boolean => t.kind === "array" && t.elem.kind === "f64";
+
+ const expr = (e: IrExpr): void => {
+ switch (e.kind) {
+ case "assignExpr":
+ this.store(sources(e.value), this.localSlot(fn, e.localId));
+ break;
+ case "call":
+ args(e.callee, e.args, 0, e.loc);
+ break;
+ case "new":
+ args(`%${e.className}.constructor`, e.args, 1, e.loc);
+ break;
+ case "virtualCall":
+ for (const impl of this.methodImpls.get(e.method) ?? []) args(impl, e.args, 0, e.loc);
+ if (!this.methodImpls.has(e.method))
+ for (const a of e.args) escape(a, e.loc, `the method '${e.method}'`);
+ break;
+ case "arrayLit": {
+ const node = sources(e)[0];
+ const spreads = new Set(e.spreads ?? []);
+ e.elems.forEach((x, i) => {
+ if (spreads.has(i)) {
+ // Spread elements copy into the literal.
+ if (node !== undefined) this.copy(sources(x), [node]);
+ if (e.type.kind === "array" && this.direct(e.type.elem))
+ this.store(elementSlot(x), `e:${typeKey(e.type)}`);
+ } else if (isNumberArray(e.type) && node !== undefined)
+ this.writes.push({ fn, target: [node], value: x, arrayText: "", literal: true });
+ else if (e.type.kind === "array" && this.holds(x.type))
+ this.store(sources(x), `e:${typeKey(e.type)}`);
+ });
+ break;
+ }
+ case "arrIntrinsic": {
+ const r = e.receiver;
+ if (isNumberArray(r.type)) {
+ const positions = ELEMENT_ARGS.get(e.method);
+ if (positions !== undefined)
+ e.args.forEach((a, i) => {
+ if (a.type.kind === "f64" && (positions === "all" || positions === i))
+ write(e.method === "with" ? e : r, a);
+ });
+ // Elements copied in from array arguments (push(...src),
+ // splice items).
+ if (COPY_IN.has(e.method))
+ for (const a of e.args) if (isNumberArray(a.type)) this.copy(sources(a), sources(r));
+ } else if (r.type.kind === "array" && this.holds(r.type.elem)) {
+ const slot = `e:${typeKey(r.type)}`;
+ for (const a of e.args) {
+ if (this.direct(a.type)) this.store(sources(a), slot);
+ else if (a.type.kind === "array" && typeKey(a.type) !== typeKey(r.type))
+ escape(a, e.loc, `Array.${e.method}`);
+ }
+ } else for (const a of e.args) escape(a, e.loc, `Array.${e.method}`);
+ break;
+ }
+ case "mapIntrinsic":
+ if (e.method === "set") {
+ const t = typeKey(e.receiver.type);
+ if (e.args[0]) this.store(sources(e.args[0]), `mk:${t}`);
+ if (e.args[1]) this.store(sources(e.args[1]), `mv:${t}`);
+ } else
+ for (const a of e.args)
+ if (e.method !== "get" && e.method !== "has" && e.method !== "delete")
+ escape(a, e.loc, `Map.${e.method}`);
+ break;
+ case "setIntrinsic":
+ if (e.method === "add" && e.args[0])
+ this.store(sources(e.args[0]), `se:${typeKey(e.receiver.type)}`);
+ break;
+ case "mapNew":
+ for (const p of e.seed ?? []) {
+ this.store(sources(p.key), `mk:${typeKey(e.type)}`);
+ this.store(sources(p.value), `mv:${typeKey(e.type)}`);
+ }
+ break;
+ case "setNew":
+ if (e.seed && e.seed.type.kind === "array")
+ this.store(elementSlot(e.seed), `se:${typeKey(e.type)}`);
+ break;
+ case "recordLit":
+ if (e.type.kind === "record") {
+ const shape = this.records.get(e.type.shapeId);
+ for (const f of e.fields) {
+ const declared = shape?.fields.some((d) => d.name === f.name) === true;
+ this.store(sources(f.value), `s:${e.type.shapeId}\0${declared ? f.name : "*"}`);
+ }
+ } else for (const f of e.fields) escape(f.value, e.loc, "an object literal");
+ break;
+ case "recordClone":
+ if (e.type.kind === "record" && e.source.type.kind === "record") {
+ const from = e.source.type.shapeId;
+ const shape = this.records.get(e.type.shapeId);
+ for (const f of shape?.fields ?? [])
+ if (this.direct(f.type))
+ this.edge(`s:${from}\0${f.name}`, `s:${e.type.shapeId}\0${f.name}`);
+ for (const o of e.overrides)
+ this.store(sources(o.value), `s:${e.type.shapeId}\0${o.name}`);
+ }
+ break;
+ case "closure": {
+ const lifted = c.functions.get(e.fnName);
+ e.captures.forEach((id, i) => {
+ const inner = lifted?.captures?.[i];
+ if (!inner || !lifted) return;
+ const outer = this.localSlot(fn, id);
+ const innerSlot = this.localSlot(lifted, inner.localId);
+ this.edge(outer, innerSlot);
+ this.edge(innerSlot, outer);
+ });
+ break;
+ }
+ case "classRef":
+ if (e.captures)
+ for (const method of c.mod.functions) {
+ if (!method.name.startsWith(`%${e.className}.`)) continue;
+ for (const cc of method.classCaptures ?? []) {
+ const id = e.captures[cc.slot];
+ if (id === undefined) continue;
+ const outer = this.localSlot(fn, id);
+ const inner = this.localSlot(method, cc.localId);
+ this.edge(outer, inner);
+ this.edge(inner, outer);
+ }
+ }
+ break;
+ case "libCall":
+ if (!READ_ONLY_LIB.test(e.fn))
+ for (const a of e.args) escape(a, a.loc, `the library call ${e.fn}`);
+ break;
+ case "callValue":
+ for (const a of e.args) escape(a, a.loc, "a call through a function value");
+ if (e.receiver) escape(e.receiver, e.receiver.loc, "a call through a function value");
+ break;
+ case "dynFrom":
+ // Plain arrays and records cross into dynamic code as deep copies;
+ // class instances and live references stay connected.
+ if (e.liveRef === true || this.containsObject(e.value.type, new Set()))
+ escape(e.value, e.loc, "dynamic (any or unknown) code");
+ break;
+ default:
+ if (PASS_THROUGH.has(e.kind) || OBSERVE.has(e.kind)) break;
+ everyExprChild(
+ e,
+ (child) => {
+ escape(child, child.loc, describe(e));
+ return true;
+ },
+ () => true,
+ );
+ }
+ everyExprChild(
+ e,
+ (child) => {
+ expr(child);
+ return true;
+ },
+ (child) => {
+ stmt(child);
+ return true;
+ },
+ );
+ };
+
+ const stmt = (s: IrStmt): void => {
+ switch (s.kind) {
+ case "varDecl":
+ if (s.init) this.store(sources(s.init), this.localSlot(fn, s.localId));
+ break;
+ case "assign":
+ this.store(sources(s.value), this.localSlot(fn, s.localId));
+ break;
+ case "return":
+ if (s.value) {
+ if (fn.async === true || fn.generator !== undefined)
+ escape(s.value, s.loc, "the result of an async function or generator");
+ else this.store(sources(s.value), `r:${fn.name}`);
+ }
+ break;
+ case "fieldSet": {
+ const family = this.slots.family(s.className, s.field);
+ if (family !== null) this.store(sources(s.value), `f:${family}`);
+ else escape(s.value, s.loc, "a field scriptc cannot follow");
+ break;
+ }
+ case "recordSet":
+ this.store(sources(s.value), `s:${s.shapeId}\0${s.field}`);
+ break;
+ case "recordKeySet": {
+ const t = s.obj.type;
+ if (t.kind === "record") {
+ const shape = this.records.get(t.shapeId);
+ const src = sources(s.value);
+ this.store(src, `s:${t.shapeId}\0*`);
+ for (const f of shape?.fields ?? [])
+ if (this.direct(f.type)) this.store(src, `s:${t.shapeId}\0${f.name}`);
+ } else escape(s.value, s.loc, "a dynamically keyed property");
+ break;
+ }
+ case "arraySet":
+ if (isNumberArray(s.arr.type)) write(s.arr, s.value);
+ else if (s.arr.type.kind === "array" && this.holds(s.arr.type.elem))
+ this.store(sources(s.value), `e:${typeKey(s.arr.type)}`);
+ break;
+ case "forOf":
+ if (this.holds(fn.locals.find((l) => l.id === s.localId)?.type ?? s.iterable.type)) {
+ const slot = this.localSlot(fn, s.localId);
+ const t = s.iterable.type;
+ if (t.kind === "array") this.store(elementSlot(s.iterable), slot);
+ else {
+ // Another iterable yields stored values or unknown ones.
+ this.store(this.contained(t), slot);
+ for (const node of this.opaqueValue(
+ fn.locals.find((l) => l.id === s.localId)?.type ?? t,
+ s.loc,
+ "an iterator",
+ ))
+ this.edge(node, slot);
+ }
+ }
+ break;
+ case "tryCatch":
+ if (s.catchLocalId !== null) {
+ const type = fn.locals.find((l) => l.id === s.catchLocalId)?.type;
+ if (type && this.holds(type))
+ for (const node of this.opaqueValue(type, s.loc, "a caught value"))
+ this.edge(node, this.localSlot(fn, s.catchLocalId));
+ }
+ break;
+ case "throw":
+ escape(s.value, s.loc, "a throw");
+ break;
+ default:
+ break;
+ }
+ everyStmtChild(
+ s,
+ (child) => {
+ expr(child);
+ return true;
+ },
+ (child) => {
+ stmt(child);
+ return true;
+ },
+ );
+ };
+ for (const s of fn.body) stmt(s);
+ }
+
+ /** Whether a value of type `t` can reach a class instance. */
+ private containsObject(t: IrType, seen: Set): boolean {
+ const key = typeKey(t);
+ if (seen.has(key)) return false;
+ seen.add(key);
+ switch (t.kind) {
+ case "object":
+ return true;
+ case "array":
+ case "set":
+ return this.containsObject(t.elem, seen);
+ case "map":
+ return this.containsObject(t.key, seen) || this.containsObject(t.value, seen);
+ case "union":
+ return (this.unions.get(t.unionId) ?? []).some((a) => this.containsObject(a, seen));
+ case "record": {
+ const shape = this.records.get(t.shapeId);
+ return (
+ (shape?.fields.some((f) => this.containsObject(f.type, seen)) ?? false) ||
+ (shape?.indexValue !== undefined && this.containsObject(shape.indexValue, seen))
+ );
+ }
+ default:
+ return false;
+ }
+ }
+
+ /* ── solving ── */
+
+ private solve(): void {
+ const c = this.c;
+ // Backward from every annotated slot: the allocations and origins whose
+ // arrays can end up in it are constrained. Forward from those: the
+ // nodes that may hold a constrained array. An array whose elements are
+ // copied into one that may be constrained must be constrained as well,
+ // which can constrain more arrays: repeat until nothing changes.
+ const constrained = new Map();
+ const expanded = new Set();
+ const roots: [string, ContractNote][] = [...this.annotated];
+ const forward: string[] = [];
+ while (roots.length > 0) {
+ for (const [root, note] of roots.splice(0)) {
+ if (expanded.has(root)) continue;
+ expanded.add(root);
+ const queue = [root];
+ while (queue.length) {
+ const node = queue.pop()!;
+ if (this.allocs.has(node) && !constrained.has(node)) {
+ constrained.set(node, note);
+ if (!this.constrainedReach.has(node)) {
+ this.constrainedReach.set(node, note);
+ forward.push(node);
+ }
+ }
+ const origin = this.opaque.get(node);
+ if (origin)
+ c.report(
+ "SC7003",
+ note.loc,
+ `the Int32[] contract on '${note.name}' cannot be checked: the array can come from ${origin.why} (${c.where(origin.loc)}), whose element stores scriptc does not see`,
+ "build the array in typed code (a literal, `new Array(n).fill(0)` or `push`) and pass it along, or remove Int32[]",
+ );
+ for (const p of this.pred.get(node) ?? NO_NODES)
+ if (!expanded.has(p)) {
+ expanded.add(p);
+ queue.push(p);
+ }
+ }
+ }
+ while (forward.length) {
+ const node = forward.pop()!;
+ const note = this.constrainedReach.get(node)!;
+ for (const next of this.succ.get(node) ?? NO_NODES)
+ if (!this.constrainedReach.has(next)) {
+ this.constrainedReach.set(next, note);
+ forward.push(next);
+ }
+ }
+ for (const copy of this.copies) {
+ const note = this.reachingNote(copy.to);
+ if (note === undefined) continue;
+ for (const from of copy.from) if (!expanded.has(from)) roots.push([from, note]);
+ }
+ }
+ const queue: string[] = [];
+ for (const node of [...this.allocs, ...this.opaque.keys()])
+ if (!constrained.has(node)) {
+ this.unconstrainedReach.add(node);
+ queue.push(node);
+ }
+ while (queue.length) {
+ const node = queue.pop()!;
+ for (const next of this.succ.get(node) ?? NO_NODES)
+ if (!this.unconstrainedReach.has(next)) {
+ this.unconstrainedReach.add(next);
+ queue.push(next);
+ }
+ }
+ for (const e of this.escapes) {
+ const note = this.reachingNote(e.nodes);
+ if (note === undefined) continue;
+ c.report(
+ "SC7003",
+ e.loc,
+ `an array that can reach '${note.name}' (Int32[]) escapes to ${e.what}, where its element stores are not checked${e.shown ? `: '${e.shown}'` : ""}`,
+ "keep Int32[] arrays in typed code, or pass a copy (`[...xs]`) that no Int32[] slot holds",
+ );
+ }
+ for (const slot of new Set([...this.annotated.keys(), ...this.constrainedReach.keys()]))
+ if (!this.unconstrainedReach.has(slot) && /^[lgfrs]:/.test(slot)) this.int32Slots.add(slot);
+ }
+
+ private reachingNote(nodes: readonly string[]): ContractNote | undefined {
+ for (const n of nodes) {
+ const note = this.constrainedReach.get(n);
+ if (note) return note;
+ }
+ return undefined;
+ }
+}
+
+function push(map: Map, key: K, value: V): void {
+ const list = map.get(key);
+ if (list) list.push(value);
+ else map.set(key, [value]);
+}
+
+function orZero(text: string): string {
+ return /^[\w.$]+(\[[^\]]*\])?$/.test(text) ? `${text} | 0` : `(${text}) | 0`;
+}
+
+function describe(e: IrExpr): string {
+ switch (e.kind) {
+ case "dynFrom":
+ case "dynObjLit":
+ case "dynArrLit":
+ case "dynCheck":
+ return "dynamic (any or unknown) code";
+ case "intrinsic":
+ return `the runtime (${e.name})`;
+ case "libCall":
+ return `the library call ${e.fn}`;
+ case "awaitExpr":
+ return "an awaited promise";
+ default:
+ return `an operation scriptc does not follow (${e.kind})`;
+ }
+}
diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts
index 0a97b07c2..eaae1b531 100644
--- a/packages/compiler/src/diagnostics/diagnostic.ts
+++ b/packages/compiler/src/diagnostics/diagnostic.ts
@@ -97,14 +97,18 @@ export function nativeCodegenDiag(
code: "SC3002" | "SC3003" | "SC3004",
message: string,
file: string,
+ hint?: string,
): ScrDiagnostic {
+ const shown =
+ hint ??
+ (code === "SC3003"
+ ? "reinstall scriptc with optional dependencies enabled for this host"
+ : undefined);
return {
code,
message,
loc: { file, start: 0, end: 0 },
- ...(code === "SC3003"
- ? { hint: "reinstall scriptc with optional dependencies enabled for this host" }
- : {}),
+ ...(shown !== undefined ? { hint: shown } : {}),
};
}
@@ -407,7 +411,7 @@ export const UNSUPPORTED: Record = {
SC1120: {
feature: "this regex feature",
milestone: "later",
- hint: "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates",
+ hint: "supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $ templates, \\k), .source/.flags, and string replace/replaceAll/split with string replacement templates",
},
// SC1121 (stateful g/y regex tests) shipped — code retired, do not reuse.
};
diff --git a/packages/compiler/src/diagnostics/render.test.ts b/packages/compiler/src/diagnostics/render.test.ts
new file mode 100644
index 000000000..c5078dd0e
--- /dev/null
+++ b/packages/compiler/src/diagnostics/render.test.ts
@@ -0,0 +1,56 @@
+import { expect, test } from "vitest";
+import type { ScrDiagnostic } from "./diagnostic.js";
+import { renderDiagnostic, renderDiagnostics } from "./render.js";
+
+function diag(file: string, start: number, end: number): ScrDiagnostic {
+ return { code: "SC2020", message: "not supported", loc: { file, start, end } };
+}
+
+test("a short line renders whole", () => {
+ const text = "const a = 1;\nconst b = a.c;\nconst d = 2;";
+ const start = text.indexOf("a.c");
+ expect(renderDiagnostic(diag("m.ts", start, start + 3), { text })).toBe(
+ [
+ "m.ts:2:11 - error SC2020: not supported",
+ "",
+ " 1 | const a = 1;",
+ " 2 | const b = a.c;",
+ " | ^~~",
+ " 3 | const d = 2;",
+ ].join("\n"),
+ );
+});
+
+test("a long line renders a window around the column", () => {
+ const line = "x".repeat(1000) + "bad" + "y".repeat(1000);
+ const text = "first\n" + line + "\nlast";
+ const start = text.indexOf("bad");
+ const out = renderDiagnostic(diag("min.js", start, start + 3), { text });
+ const lines = out.split("\n");
+ expect(lines[0]).toBe("min.js:2:1001 - error SC2020: not supported");
+ // the window starts 120 characters before the column
+ expect(lines[3]).toBe(` 2 | …${"x".repeat(120)}bad${"y".repeat(277)}…`);
+ expect(lines[4]).toBe(` | ${" ".repeat(121)}^~~`);
+ // context lines share the window: "first" and "last" start before it
+ expect(lines[2]).toBe(" 1 | …");
+ expect(lines[5]).toBe(" 3 | …");
+});
+
+test("a window at the line's end keeps the last characters", () => {
+ const line = "a".repeat(600) + "end";
+ const start = line.indexOf("end");
+ const out = renderDiagnostic(diag("e.js", start, start + 3), { text: line });
+ const lines = out.split("\n");
+ expect(lines[2]).toBe(` 1 | …${"a".repeat(397)}end`);
+ expect(lines[3]).toBe(` | ${" ".repeat(398)}^~~`);
+});
+
+test("thousands of diagnostics on a minified line render without exhausting string length", () => {
+ // one 2 MB line with 20,000 diagnostics: rendering every frame line whole
+ // would need tens of gigabytes
+ const text = "a;".repeat(1_000_000);
+ const diags = Array.from({ length: 20_000 }, (_, i) => diag("bundle.js", i * 100, i * 100 + 1));
+ const out = renderDiagnostics(diags, new Map([["bundle.js", text]]));
+ expect(out.split(" - error SC2020: ").length - 1).toBe(20_000);
+ expect(out.length).toBeLessThan(20_000 * 1_000);
+});
diff --git a/packages/compiler/src/diagnostics/render.ts b/packages/compiler/src/diagnostics/render.ts
index 7fd68910f..e5a75c6bc 100644
--- a/packages/compiler/src/diagnostics/render.ts
+++ b/packages/compiler/src/diagnostics/render.ts
@@ -1,6 +1,8 @@
/* Terminal rendering: file:line:col, a hand-rolled code frame with ±1 line
* of context and a ^~~~ underline sized to the span, optional hint line,
- * and the optional attributed profile-note line after it.
+ * and the optional attributed profile-note line after it. A source line
+ * longer than FRAME_MAX_WIDTH (minified sources) shows a window of that
+ * width around the diagnostic's column, with "…" where text is cut.
*/
import type { ScrDiagnostic } from "./diagnostic.js";
@@ -11,6 +13,27 @@ export interface RenderOptions {
interface SourceLookup {
/** Full text of the file the diagnostic points into. */
text: string;
+ /** The text's lines and their start offsets; computed from `text` when
+ * absent. renderDiagnostics computes them once per file. */
+ index?: LineIndex;
+}
+
+interface LineIndex {
+ lines: string[];
+ lineStarts: number[];
+}
+
+/** Frame lines longer than this many characters show a window of it. */
+const FRAME_MAX_WIDTH = 400;
+/** Characters a windowed frame keeps before the diagnostic's column. */
+const FRAME_LEAD = 120;
+
+function lineIndex(text: string): LineIndex {
+ const lineStarts = [0];
+ for (let i = 0; i < text.length; i++) {
+ if (text[i] === "\n") lineStarts.push(i + 1);
+ }
+ return { lines: text.split("\n"), lineStarts };
}
const RED = "\x1b[31m";
@@ -33,32 +56,52 @@ export function renderDiagnostic(
const frame: string[] = [];
if (source) {
- const lineStarts = [0];
- for (let i = 0; i < source.text.length; i++) {
- if (source.text[i] === "\n") lineStarts.push(i + 1);
- }
+ const { lines, lineStarts } = source.index ?? lineIndex(source.text);
+ // the last line starting at or before the diagnostic
let lo = 0;
- while (lo + 1 < lineStarts.length && lineStarts[lo + 1]! <= diag.loc.start) lo++;
+ let hi = lineStarts.length - 1;
+ while (lo < hi) {
+ const mid = (lo + hi + 1) >> 1;
+ if (lineStarts[mid]! <= diag.loc.start) lo = mid;
+ else hi = mid - 1;
+ }
lineNum = lo + 1;
colNum = diag.loc.start - lineStarts[lo]! + 1;
- const lines = source.text.split("\n");
+ const lineText = lines[lineNum - 1] ?? "";
+ // The window every frame line shows: the whole line, or for a long
+ // line FRAME_MAX_WIDTH characters starting FRAME_LEAD before the column.
+ const windowStart =
+ lineText.length > FRAME_MAX_WIDTH
+ ? Math.max(0, Math.min(colNum - 1 - FRAME_LEAD, lineText.length - FRAME_MAX_WIDTH))
+ : 0;
+ const shown = (text: string): string => {
+ if (windowStart === 0 && text.length <= FRAME_MAX_WIDTH) return text;
+ const end = windowStart + FRAME_MAX_WIDTH;
+ return (
+ (windowStart > 0 ? "…" : "") + text.slice(windowStart, end) + (text.length > end ? "…" : "")
+ );
+ };
const gutterWidth = String(Math.min(lineNum + 1, lines.length)).length;
const emitLine = (n: number) => {
const text = lines[n - 1];
if (text === undefined) return;
- frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + text);
+ frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + shown(text));
};
emitLine(lineNum - 1);
emitLine(lineNum);
- const lineText = lines[lineNum - 1] ?? "";
+ const caretCol = colNum - 1 - windowStart + (windowStart > 0 ? 1 : 0);
+ const visibleEnd =
+ windowStart === 0 && lineText.length <= FRAME_MAX_WIDTH
+ ? lineText.length
+ : Math.min(lineText.length, windowStart + FRAME_MAX_WIDTH);
const spanOnLine = Math.max(
1,
- Math.min(diag.loc.end - diag.loc.start, lineText.length - (colNum - 1)),
+ Math.min(diag.loc.end - diag.loc.start, visibleEnd - (colNum - 1)),
);
frame.push(
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
- " ".repeat(colNum - 1) +
+ " ".repeat(caretCol) +
c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)),
);
emitLine(lineNum + 1);
@@ -94,10 +137,17 @@ export function renderDiagnostics(
const sorted = [...diags].sort(
(a, b) => a.loc.file.localeCompare(b.loc.file) || a.loc.start - b.loc.start,
);
+ const indexes = new Map();
return sorted
.map((d) => {
const text = sourceTextByFile.get(d.loc.file);
- return renderDiagnostic(d, text === undefined ? undefined : { text }, opts);
+ if (text === undefined) return renderDiagnostic(d, undefined, opts);
+ let index = indexes.get(d.loc.file);
+ if (index === undefined) {
+ index = lineIndex(text);
+ indexes.set(d.loc.file, index);
+ }
+ return renderDiagnostic(d, { text, index }, opts);
})
.join("\n\n");
}
diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts
index bd396dacd..24462a2ce 100644
--- a/packages/compiler/src/frontend/contract-types.ts
+++ b/packages/compiler/src/frontend/contract-types.ts
@@ -30,6 +30,9 @@ export interface ContractNote {
loc: SrcLoc;
/** The annotated declaration's name, for messages. */
name: string;
+ /** `Int32[]` (also `readonly Int32[]`, `Array`, `ReadonlyArray`):
+ * the contract covers the elements of the annotated array. */
+ elements?: true;
}
/** An Acyclic type: a class (by its declaration's location key, like
@@ -202,7 +205,7 @@ function unsupported(out: ContractAnnotations, node: ts.Node, marker: ContractMa
marker === "NoThrow"
? "NoThrow wraps a function's written return type"
: marker === "Int32"
- ? "Int32 is checked on parameters, variables, class instance fields and function results"
+ ? "Int32 and Int32[] are checked on parameters, variables, class instance fields and function results"
: marker === "ProgramLifetime"
? "ProgramLifetime is checked in the implements clause of a class declaration"
: "Immutable is checked on parameters, variables, class instance fields and function results";
@@ -215,7 +218,9 @@ function unsupported(out: ContractAnnotations, node: ts.Node, marker: ContractMa
? "write it as the function's return type annotation: function f(): NoThrow"
: marker === "ProgramLifetime"
? "declare it on the class itself: class Node implements ProgramLifetime { ... }"
- : `write ${marker} directly as the annotation of the declaration; type aliases, interface members and types nested in other types (arrays, unions, type arguments) are not followed`,
+ : marker === "Int32"
+ ? "write Int32 or Int32[] directly as the annotation of the declaration; type aliases, interface members and types nested in other types (unions, nested arrays, other type arguments) are not followed"
+ : `write ${marker} directly as the annotation of the declaration; type aliases, interface members and types nested in other types (arrays, unions, type arguments) are not followed`,
),
);
}
@@ -299,16 +304,47 @@ function place(
): void {
let node: ts.Node = ref;
let parent = ref.parent;
+ // Int32[] (readonly Int32[], Array, ReadonlyArray): the
+ // contract covers the elements of the annotated array.
+ let elements = false;
+ if (marker === "Int32" && parent !== undefined) {
+ if (ts.isArrayTypeNode(parent) && parent.elementType === ref) {
+ node = parent;
+ parent = parent.parent;
+ elements = true;
+ if (
+ parent !== undefined &&
+ ts.isTypeOperatorNode(parent) &&
+ parent.operator === ts.SyntaxKind.ReadonlyKeyword
+ ) {
+ node = parent;
+ parent = parent.parent;
+ }
+ } else if (
+ ts.isTypeReferenceNode(parent) &&
+ ts.isIdentifier(parent.typeName) &&
+ (parent.typeName.text === "Array" || parent.typeName.text === "ReadonlyArray") &&
+ parent.typeArguments?.length === 1 &&
+ parent.typeArguments[0] === ref
+ ) {
+ node = parent;
+ parent = parent.parent;
+ elements = true;
+ }
+ }
// NoThrow / NoThrow>: the inner marker is the result
// contract of the function the NoThrow annotates.
if (marker !== "NoThrow" && parent !== undefined && ts.isTypeReferenceNode(parent)) {
const wrapper = markerOf(checker, parent);
- if (wrapper === "NoThrow" && parent.typeArguments?.[0] === ref) {
+ if (wrapper === "NoThrow" && parent.typeArguments?.[0] === node) {
node = parent;
parent = parent.parent;
}
}
- const note = (name: string): ContractNote => ({ marker, loc: locOf(ref), name });
+ const note = (name: string): ContractNote =>
+ elements
+ ? { marker, loc: locOf(ref), name, elements: true }
+ : { marker, loc: locOf(ref), name };
if (parent === undefined) return unsupported(out, ref, marker);
if (ts.isFunctionLike(parent) && parent.type === node) {
if (!ts.isFunctionLikeDeclaration(parent)) return unsupported(out, ref, marker);
diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts
index e6a06151d..816294e08 100644
--- a/packages/compiler/src/frontend/lowering/binding-analysis.ts
+++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts
@@ -2,6 +2,7 @@ import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { isJsSourceFile } from "../program.js";
import { dynFallbackType } from "./lowerer.js";
+import { isGenericCallableMemberType } from "../type-mapper.js";
/** True when nothing in `sym`'s DECLARING FILE ever writes it after the
* initializer: assignments (plain and compound, destructuring targets
@@ -199,7 +200,8 @@ export function nullishGenericBindingUnitOf(
// (a query can even panic upstream — the 1e999 checker bug).
const unit = nullishValueUnitOf(lowerer, sym);
if (unit === null) return null;
- const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym));
+ const declared = lowerer.checker.getTypeOfSymbol(sym);
+ const mapped = lowerer.mapTypeOf(declared);
if (mapped !== null) {
// Only the EMPTY interned shape qualifies among record mappings —
// the all-generic-signature interface (`I`) whose struct has
@@ -210,9 +212,16 @@ export function nullishGenericBindingUnitOf(
// so claiming the binding would fence working programs.
if (mapped.kind !== "record") return null;
const shape = lowerer.shapes.get(mapped.shapeId);
+ // Erased generic members are function slots, not data: an interface
+ // of generic signatures only keeps the no-storage story.
+ const onlyGenericMembers = lowerer.checker
+ .getPropertiesOfType(declared)
+ .every((property) =>
+ isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(property), lowerer.checker),
+ );
if (
!shape ||
- shape.fields.length > 0 ||
+ (shape.fields.length > 0 && !onlyGenericMembers) ||
shape.tuple !== undefined ||
shape.indexValue !== undefined
) {
diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts
index 810fd6d2f..037b3e5a7 100644
--- a/packages/compiler/src/frontend/lowering/builtin-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts
@@ -1,9 +1,14 @@
import { lowerReadlineCursorCall } from "./builtins/readline.js";
-import { lowerReceiveMessageOnPort } from "./builtins/workers.js";
+import {
+ lowerEnvironmentData,
+ lowerReceiveMessageOnPort,
+ lowerUncloneable,
+ lowerUntransferable,
+} from "./builtins/workers.js";
import * as ts from "../ts7/adapter.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { DYN, type IrExpr, type SrcLoc } from "../../ir/ir.js";
-import { lowerEventsOnce } from "./lower-event-emitter.js";
+import { lowerEventsEmitterHelper, lowerEventsOnce } from "./lower-event-emitter.js";
import type { Lowerer } from "./lowerer.js";
import { lowerTimersMemberCall } from "./lower-timers.js";
import { lowerStreamModuleCall } from "./lower-stream.js";
@@ -64,6 +69,10 @@ export function lowerBuiltinCall(
return lowerAddAbortListener(lowerer, expr, loc);
}
if (bi.module === "events" && bi.member === "once") return lowerEventsOnce(lowerer, expr, loc);
+ if (bi.module === "events") {
+ const helper = lowerEventsEmitterHelper(lowerer, expr, bi.member, loc);
+ if (helper) return helper;
+ }
if (bi.module === "timers") {
const timersServed = lowerTimersMemberCall(lowerer, expr, bi.member, loc);
if (timersServed) return timersServed;
@@ -97,6 +106,21 @@ export function lowerBuiltinCall(
if (bi.module === "worker_threads" && bi.member === "receiveMessageOnPort") {
return lowerReceiveMessageOnPort(lowerer, expr);
}
+ if (
+ bi.module === "worker_threads" &&
+ (bi.member === "setEnvironmentData" || bi.member === "getEnvironmentData")
+ ) {
+ return lowerEnvironmentData(lowerer, expr, bi.member);
+ }
+ if (
+ bi.module === "worker_threads" &&
+ (bi.member === "markAsUntransferable" || bi.member === "isMarkedAsUntransferable")
+ ) {
+ return lowerUntransferable(lowerer, expr, bi.member);
+ }
+ if (bi.module === "worker_threads" && bi.member === "markAsUncloneable") {
+ return lowerUncloneable(lowerer, expr);
+ }
const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member);
if (!builtinFn) {
lowerer.noLowering(
diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts
index 023d9abf2..fa880bdc9 100644
--- a/packages/compiler/src/frontend/lowering/builtins/dates.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts
@@ -1,7 +1,17 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, own } from "../lowerer.js";
import { locOf } from "../../program.js";
-import { F64, type IrExpr, type IrLibFn, STRING } from "../../../ir/ir.js";
+import {
+ BOOL,
+ F64,
+ type IrExpr,
+ type IrLibFn,
+ type IrStmt,
+ type IrType,
+ NULL_T,
+ STRING,
+ typeKey,
+} from "../../../ir/ir.js";
const DATE_GETTER_FNS: Readonly> = {
getFullYear: "date.getFullYear",
@@ -24,7 +34,7 @@ const DATE_GETTER_FNS: Readonly> = {
};
const DATE_METHOD_HINT =
- "getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
+ "getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
/** The Date slice: statics plus read-only Date values backed by one
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
@@ -80,6 +90,7 @@ export function lowerDateCall(
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
+ if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
if (call.arguments.length !== 0) {
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
}
@@ -113,3 +124,77 @@ export function lowerDateCall(
lowerer.checker.getSymbolAtLocation(access.name),
);
}
+
+/** `d.toJSON(key?)`: Node answers null for an invalid date and the
+ * `toISOString()` string otherwise, as `string | null` (the lib types say
+ * `string`). The argument is evaluated and ignored, as the built-in does. */
+function lowerDateToJson(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr {
+ const loc = locOf(call);
+ if (call.arguments.length > 1 || call.arguments.some((a) => ts.isSpreadElement(a)))
+ lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.length} arguments`, call);
+ const raw = lowerer.lowerExpr(access.expression);
+ const receiver: IrExpr =
+ raw.type.kind === "dyn"
+ ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc }
+ : raw;
+ if (receiver.type.kind !== "date")
+ lowerer.badType(access.expression, lowerer.typeOf(access.expression));
+ const date = lowerer.declareHiddenLocal("%date", receiver.type);
+ const stmts: IrStmt[] = [{ kind: "varDecl", localId: date.id, init: receiver, loc }];
+ const key = call.arguments[0];
+ if (key) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(key), loc });
+ const value: IrExpr = { kind: "varRef", localId: date.id, type: receiver.type, loc };
+ const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc };
+ const type = dateJsonType(lowerer);
+ return {
+ kind: "seqExpr",
+ stmts,
+ result: {
+ kind: "ternary",
+ // An invalid date's time value is NaN, the one value unequal to itself.
+ cond: { kind: "bin", op: "!==", left: time, right: time, type: BOOL, loc },
+ then: lowerer.coerceToExpected({ kind: "unitLit", unit: "null", type: NULL_T, loc }, type),
+ else_: lowerer.coerceToExpected(
+ { kind: "libCall", fn: "date.toISOStringValue", args: [value], type: STRING, loc },
+ type,
+ ),
+ type,
+ loc,
+ },
+ type,
+ loc,
+ };
+}
+
+/** `string | null`, the type of a Date's `toJSON()` result. */
+export function dateJsonType(lowerer: Lowerer): IrType {
+ const arms = [STRING, NULL_T].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
+ return { kind: "union", unionId: lowerer.unions.intern(arms) };
+}
+
+/** A Date `toJSON()` call, or a binding initialized by one
+ * (Lowerer.dateJsonBindings): its value can be null where the lib types
+ * say string. */
+export function isDateJsonSource(lowerer: Lowerer, node: ts.Node): boolean {
+ while (ts.isParenthesizedExpression(node)) node = node.expression;
+ if (ts.isIdentifier(node)) {
+ const symbol =
+ node.parent && ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
+ ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
+ : lowerer.checker.getSymbolAtLocation(node);
+ return symbol !== undefined && lowerer.dateJsonBindings.has(symbol);
+ }
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return false;
+ const access = node.expression;
+ return (
+ access.name.text === "toJSON" &&
+ !node.questionDotToken &&
+ !access.questionDotToken &&
+ lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind === "date" &&
+ lowerer.isStdlibMember(access)
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts
new file mode 100644
index 000000000..469a66ace
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts
@@ -0,0 +1,136 @@
+import * as ts from "../../ts7/adapter.js";
+import { type Lowerer } from "../lowerer.js";
+import { locOf } from "../../program.js";
+import { newFnCtx } from "../function-context.js";
+import { BOOL, DYN, VOID, type IrExpr, type IrType } from "../../../ir/ir.js";
+import { varRef } from "../../../ir/build.js";
+
+/** Whether `node`'s static type is the standard library's DisposableStack. */
+function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean {
+ const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
+ for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
+ if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true;
+ }
+ return false;
+}
+
+/** `new DisposableStack()`: a native stack handle (scr_symbol.c). Its
+ * members are function values and getters of the handle, reached through
+ * the checked-dynamic method and property paths. Null for other
+ * constructors. */
+export function lowerDisposableStackNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
+ if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "DisposableStack") return null;
+ const symbol = lowerer.resolveValueSymbol(expr.expression);
+ if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
+ if ((expr.arguments ?? []).length !== 0) return null;
+ return { kind: "libCall", fn: "disposable.stackNew", args: [], type: DYN, loc: locOf(expr) };
+}
+
+/** `x instanceof DisposableStack`, or null when the right side is not the
+ * standard library's DisposableStack. */
+export function lowerDisposableStackInstanceOf(
+ lowerer: Lowerer,
+ expr: ts.BinaryExpression,
+): IrExpr | null {
+ if (!lowerer.isStdlibGlobal(expr.right, "DisposableStack")) return null;
+ const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
+ return { kind: "libCall", fn: "disposable.stackIs", args: [value], type: BOOL, loc: locOf(expr) };
+}
+
+/** `stack.use(value)` where `value` is a class instance. The dyn view of a
+ * class instance has no methods, so the stack receives a function that
+ * calls the class's [Symbol.dispose] method on the value, and the call
+ * answers the value at its static type. Other values take the
+ * checked-dynamic method path, which reads the value's [Symbol.dispose].
+ * Null when the call is not that form. */
+export function lowerDisposableStackUseCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (call.questionDotToken || access.questionDotToken) return null;
+ if (access.name.text !== "use" || call.arguments.length !== 1) return null;
+ const argNode = call.arguments[0]!;
+ if (ts.isSpreadElement(argNode)) return null;
+ if (!isStdlibDisposableStack(lowerer, access.expression)) return null;
+ const valueT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
+ if (valueT?.kind === "union") {
+ const arms = lowerer.unions.get(valueT.unionId)?.arms ?? [];
+ if (arms.some((arm) => arm.kind === "object")) {
+ lowerer.unsupported(
+ "SC1090",
+ argNode,
+ "DisposableStack.use on a union with class instances (narrow the value to the class first)",
+ );
+ }
+ }
+ if (valueT?.kind !== "object") return null;
+ const loc = locOf(call);
+ const className = valueT.className;
+ const info = lowerer.classes.get(className);
+ if (!info) lowerer.flushDeferredClass(className);
+ const found = info ? lowerer.findMethodOn(info, "sym:dispose") : null;
+ if (!found) return null;
+ if (found.sig.params.length !== 0) {
+ lowerer.unsupported("SC1090", argNode, "[Symbol.dispose] methods with parameters");
+ }
+ const stack = lowerer.lowerExpr(access.expression);
+ const value = lowerer.lowerExpr(argNode);
+ const key = `disposableStack.use:${className}`;
+ let helper = lowerer.valueHelpers.get(key);
+ if (!helper) {
+ helper = `%disposableStack.use.${lowerer.valueHelpers.size}`;
+ lowerer.valueHelpers.set(key, helper);
+ const context = newFnCtx(false, null, null, VOID);
+ lowerer.fnStack.push(context);
+ try {
+ const param = lowerer.declareHiddenLocal("value", valueT);
+ const self = varRef(param.id, valueT, loc);
+ lowerer.liftedFns.push({
+ name: helper,
+ params: [{ localId: param.id, name: "value", type: valueT }],
+ returnType: VOID,
+ locals: context.locals,
+ body: [
+ {
+ kind: "exprStmt",
+ expr: lowerer.accessorCall(className, "sym:dispose", self, [], found.sig.ret, loc),
+ loc,
+ },
+ ],
+ loc,
+ });
+ } finally {
+ lowerer.fnStack.pop();
+ }
+ }
+ const stackLocal = lowerer.declareHiddenLocal("%disposableStack", DYN);
+ const valueLocal = lowerer.declareHiddenLocal("%disposableValue", valueT);
+ const fnT: IrType = { kind: "func", params: [valueT], ret: VOID };
+ const dispose: IrExpr = { kind: "closure", fnName: helper, captures: [], type: fnT, loc };
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: stackLocal.id, init: stack, loc },
+ { kind: "varDecl", localId: valueLocal.id, init: value, loc },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "disposable.stackUseCallback",
+ args: [
+ varRef(stackLocal.id, DYN, loc),
+ lowerer.coerceToExpected(varRef(valueLocal.id, valueT, loc), DYN),
+ lowerer.coerceToExpected(dispose, DYN),
+ ],
+ type: BOOL,
+ loc,
+ },
+ loc,
+ },
+ ],
+ result: varRef(valueLocal.id, valueT, loc),
+ type: valueT,
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts
index 3bc04ce07..5947f3714 100644
--- a/packages/compiler/src/frontend/lowering/builtins/errors.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts
@@ -1,7 +1,7 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
-import { DYN, F64, type IrExpr, STRING } from "../../../ir/ir.js";
+import { BOOL, CAUGHT, DYN, F64, type IrExpr, type IrType, STRING } from "../../../ir/ir.js";
const ASSERTION_ERROR_PROPERTIES: ReadonlySet = new Set([
"actual",
@@ -158,3 +158,107 @@ export function lowerErrorCodeProperty(
loc: locOf(expr),
};
}
+
+/** Whether values of `type` are errors: class instances whose class is
+ * %Error or extends it. A subclass of a class that does not extend Error
+ * cannot be an error either, so the answer depends on the static class
+ * alone. */
+function isErrorClassType(lowerer: Lowerer, type: IrType): boolean {
+ return (
+ type.kind === "object" &&
+ (type.className === "%Error" || lowerer.isSubclassOf(type.className, "%Error"))
+ );
+}
+
+/** `Error.isError(value)`. A catch binding tests its payload against the
+ * Error class, an `unknown` value tests the checked-dynamic tree's error
+ * encoding, and a union tests its runtime tag against the arms whose class
+ * extends Error. Every other type answers statically: true for class
+ * instances whose class extends Error, false otherwise. The argument is
+ * evaluated in every case. Null when the callee is not the standard
+ * library's Error.isError. */
+export function lowerErrorIsErrorCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (call.questionDotToken || access.questionDotToken) return null;
+ if (lowerer.stdlibGlobalMember(access, "Error") !== "isError") return null;
+ if (call.arguments.length !== 1) return null;
+ const argNode = call.arguments[0]!;
+ const loc = locOf(call);
+ const caughtLocal = lowerer.caughtLocalOf(argNode);
+ if (caughtLocal) {
+ return {
+ kind: "caughtTest",
+ value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc },
+ test: "instanceof",
+ className: "%Error",
+ type: BOOL,
+ loc,
+ };
+ }
+ const arg = lowerer.lowerExpr(argNode);
+ if (arg.type.kind === "dyn") {
+ return { kind: "dynTest", test: "error", value: arg, type: BOOL, loc };
+ }
+ if (arg.type.kind === "union") {
+ const union = lowerer.unions.get(arg.type.unionId);
+ if (!union || union.arms.some((a) => a.kind === "dyn" || a.kind === "caught")) {
+ lowerer.unsupported(
+ "SC1090",
+ argNode,
+ `Error.isError on '${lowerer.fmt(arg.type)}' values (narrow first, or pass the value as 'unknown')`,
+ );
+ }
+ const local = lowerer.declareHiddenLocal("%isErrorValue", arg.type);
+ const value: IrExpr = { kind: "varRef", localId: local.id, type: arg.type, loc };
+ let result: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
+ union.arms.forEach((arm, tag) => {
+ if (!isErrorClassType(lowerer, arm)) return;
+ const test: IrExpr = {
+ kind: "unionIsTag",
+ unionId: union.id,
+ tag,
+ negated: false,
+ value,
+ type: BOOL,
+ loc,
+ };
+ result =
+ result.kind === "boolLit"
+ ? test
+ : { kind: "logical", op: "||", left: result, right: test, type: BOOL, loc };
+ });
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "varDecl", localId: local.id, init: arg, loc }],
+ result,
+ type: BOOL,
+ loc,
+ };
+ }
+ const result: IrExpr = {
+ kind: "boolLit",
+ value: isErrorClassType(lowerer, arg.type),
+ type: BOOL,
+ loc,
+ };
+ // Literals and local reads have no effects to keep, and a bare unit
+ // literal is not a statement.
+ if (
+ arg.kind === "varRef" ||
+ arg.kind === "unitLit" ||
+ arg.kind === "numLit" ||
+ arg.kind === "strLit" ||
+ arg.kind === "boolLit"
+ )
+ return result;
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: arg, loc }],
+ result,
+ type: BOOL,
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts
index 475cfd51c..a73c9d5e7 100644
--- a/packages/compiler/src/frontend/lowering/builtins/json.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/json.ts
@@ -1,3 +1,4 @@
+import { isDateJsonSource } from "./dates.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
@@ -82,7 +83,12 @@ export function lowerJsonMethodCall(
// conversion so a stale capture cannot read an impossible payload.
// Optional strings have a serializer that preserves a missing root;
// an unchecked array read can still be absent despite its checker type.
- if (value.type.kind === "union" && optionalStringTags(lowerer, value.type) === null) {
+ // A Date's toJSON() result keeps its null: the lib types say string.
+ if (
+ value.type.kind === "union" &&
+ optionalStringTags(lowerer, value.type) === null &&
+ !isDateJsonSource(lowerer, argNode)
+ ) {
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(argNode));
if (narrowed && !isUnitType(narrowed) && narrowed.kind !== "void") {
const helper =
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
index dc141045f..58ac85aba 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
@@ -40,6 +40,7 @@ import { lowerForkCall } from "./child-process.js";
import {
lowerOsCpusCall,
lowerOsGetPriorityCall,
+ lowerOsSetPriorityCall,
lowerOsNetworkInterfacesCall,
lowerOsUserInfoCall,
lowerPathFormatCall,
@@ -163,6 +164,9 @@ export function lowerBuiltinModuleCall(
if (bi.module === "os" && bi.member === "cpus") {
return lowerOsCpusCall(lowerer, expr, loc);
}
+ if (bi.module === "os" && bi.member === "setPriority") {
+ return lowerOsSetPriorityCall(lowerer, expr, loc);
+ }
if (bi.module === "os" && bi.member === "getPriority") {
return lowerOsGetPriorityCall(lowerer, expr, loc);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/net-address.ts b/packages/compiler/src/frontend/lowering/builtins/net-address.ts
new file mode 100644
index 000000000..b49546d8c
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/net-address.ts
@@ -0,0 +1,168 @@
+import * as ts from "../../ts7/adapter.js";
+import { type Lowerer } from "../lowerer.js";
+import { locOf } from "../../program.js";
+import { dynUndefinedExpr } from "../../../ir/build.js";
+import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
+import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
+import { builtinImportOf } from "./module-bindings.js";
+
+/* node:net's SocketAddress and BlockList are native checked-dynamic
+ * handles (scr_net.c): both types map to dyn, their getters are handle
+ * property reads, their methods dispatch through the handle, and these
+ * helpers lower the class's uses that a dyn value cannot answer by
+ * itself (construction, the static members and instanceof). */
+
+type NetAddressClass = "SocketAddress" | "BlockList";
+
+/** The SocketAddress or BlockList class in any node:net spelling. */
+export function netAddressClassOf(lowerer: Lowerer, node: ts.Expression): NetAddressClass | null {
+ const builtin = ts.isIdentifier(node)
+ ? builtinImportOf(lowerer, node)
+ : ts.isPropertyAccessExpression(node) && !node.questionDotToken
+ ? lowerer.builtinMemberOf(node)
+ : null;
+ if (builtin?.module !== "net") return null;
+ return builtin.member === "SocketAddress" || builtin.member === "BlockList"
+ ? builtin.member
+ : null;
+}
+
+/** The class of a stdlib SocketAddress- or BlockList-typed value. */
+function netAddressValueClass(lowerer: Lowerer, node: ts.Expression): NetAddressClass | null {
+ const type = lowerer.typeOf(node);
+ const sym = type.getAliasSymbol() ?? type.getSymbol();
+ if (!sym || (sym.name !== "SocketAddress" && sym.name !== "BlockList")) return null;
+ return lowerer.isStdlibSymbol(sym) ? sym.name : null;
+}
+
+/** `new SocketAddress(options?)` and `new BlockList()`: the argument
+ * vector goes to the runtime, which validates it in Node's order. */
+export function lowerNetAddressNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
+ const cls = netAddressClassOf(lowerer, expr.expression);
+ if (cls === null) return null;
+ const args = expr.arguments ?? [];
+ if (args.some(ts.isSpreadElement)) return null;
+ const loc = locOf(expr);
+ const values = args.map((arg) =>
+ ts.isObjectLiteralExpression(arg)
+ ? lowerDynObjectLiteral(lowerer, arg)
+ : lowerer.lowerExprExpecting(arg, DYN),
+ );
+ return {
+ kind: "libCall",
+ fn: cls === "SocketAddress" ? "net.socketAddressNew" : "net.blockListNew",
+ args: [{ kind: "dynArrLit", elems: values, type: DYN, loc }],
+ type: DYN,
+ loc,
+ };
+}
+
+const STATIC_CALLS: Record<
+ string,
+ { cls: NetAddressClass; fn: IrLibFn; result: typeof DYN | typeof BOOL }
+> = {
+ parse: { cls: "SocketAddress", fn: "net.socketAddressParse", result: DYN },
+ isSocketAddress: { cls: "SocketAddress", fn: "net.isSocketAddress", result: BOOL },
+ isBlockList: { cls: "BlockList", fn: "net.isBlockList", result: BOOL },
+};
+
+const INSTANCE_METHODS: Record> = {
+ SocketAddress: new Set(["toJSON"]),
+ BlockList: new Set([
+ "addAddress",
+ "addAddresses",
+ "addRange",
+ "addSubnet",
+ "addCIDR",
+ "addCIDRs",
+ "removeAddress",
+ "removeRange",
+ "removeSubnet",
+ "removeCIDR",
+ "clear",
+ "check",
+ "toJSON",
+ "fromJSON",
+ ]),
+};
+
+/** SocketAddress.parse(), SocketAddress.isSocketAddress() and
+ * BlockList.isBlockList(); and the instance methods, which the handle
+ * answers as methods, so they dispatch through dynInvoke rather than as
+ * a member read and a call. */
+export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
+ const access = call.expression;
+ if (access.questionDotToken || call.arguments.some(ts.isSpreadElement)) return null;
+ const name = access.name.text;
+ const loc = locOf(call);
+ const statics = Object.hasOwn(STATIC_CALLS, name) ? STATIC_CALLS[name]! : null;
+ if (statics !== null && netAddressClassOf(lowerer, access.expression) === statics.cls) {
+ // The member takes one value; any others still evaluate, in order.
+ const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
+ const first: IrExpr = args[0] ?? dynUndefinedExpr(loc);
+ const result: IrExpr = {
+ kind: "libCall",
+ fn: statics.fn,
+ args: [first],
+ type: statics.result,
+ loc,
+ };
+ if (args.length <= 1) return result;
+ const local = lowerer.declareHiddenLocal("%netAddressResult", statics.result);
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: local.id, init: result, loc },
+ ...args.slice(1).map((arg) => ({ kind: "exprStmt" as const, expr: arg, loc: arg.loc })),
+ ],
+ result: { kind: "varRef", localId: local.id, type: statics.result, loc },
+ type: statics.result,
+ loc,
+ };
+ }
+ const cls = netAddressValueClass(lowerer, access.expression);
+ if (cls === null || !INSTANCE_METHODS[cls].has(name)) return null;
+ return {
+ kind: "dynInvoke",
+ recv: lowerer.lowerExprExpecting(access.expression, DYN),
+ method: name,
+ calleeName: access.getText(),
+ args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
+ type: DYN,
+ loc,
+ };
+}
+
+/** `BlockList.PRIVATE_RANGES`: one frozen array of CIDR strings. */
+export function lowerNetAddressStaticRead(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (expr.questionDotToken || expr.name.text !== "PRIVATE_RANGES") return null;
+ if (netAddressClassOf(lowerer, expr.expression) !== "BlockList") return null;
+ return {
+ kind: "libCall",
+ fn: "net.blockListPrivateRanges",
+ args: [],
+ type: DYN,
+ loc: locOf(expr),
+ };
+}
+
+/** `v instanceof SocketAddress` / `v instanceof BlockList`. */
+export function lowerNetAddressInstanceof(
+ lowerer: Lowerer,
+ expr: ts.BinaryExpression,
+): IrExpr | null {
+ const cls = netAddressClassOf(lowerer, expr.right);
+ if (cls === null) return null;
+ const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
+ return {
+ kind: "libCall",
+ fn: cls === "SocketAddress" ? "net.isSocketAddress" : "net.isBlockList",
+ args: [value],
+ type: BOOL,
+ loc: locOf(expr),
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts
index 051981f09..b88b97eb6 100644
--- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts
@@ -10,6 +10,7 @@ import {
STRING,
type SrcLoc,
UNDEFINED_T,
+ VOID,
arrayOf,
typeKey,
} from "../../../ir/ir.js";
@@ -511,6 +512,29 @@ export function lowerPathFormatCall(
return { kind: "call", callee: helper, args: [value], type: STRING, loc };
}
+/** `os.setPriority([pid, ]priority)` — the one-argument form is pid 0
+ * (Node's `if (priority === undefined) { priority = pid; pid = 0; }`);
+ * the runtime validates both. */
+export function lowerOsSetPriorityCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ loc: SrcLoc,
+): IrExpr {
+ const args = call.arguments;
+ if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(
+ "setPriority with these arguments",
+ call,
+ "setPriority(priority) or setPriority(pid, priority) takes numbers",
+ );
+ }
+ const [pid, priority] =
+ args.length === 1
+ ? [numLit(0, loc), lowerer.lowerExprExpecting(args[0]!, F64)]
+ : [lowerer.lowerExprExpecting(args[0]!, F64), lowerer.lowerExprExpecting(args[1]!, F64)];
+ return { kind: "libCall", fn: "os.setPriority", args: [pid, priority], type: VOID, loc };
+}
+
/** `os.getPriority(pid?)` — an omitted or undefined pid is 0 (Node's
* `if (pid === undefined) pid = 0`); the runtime validates the rest. */
export function lowerOsGetPriorityCall(
diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts
index 2aa1a47ce..97bf1e963 100644
--- a/packages/compiler/src/frontend/lowering/builtins/process.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/process.ts
@@ -362,10 +362,10 @@ export function lowerProcessProperty(
if (member === "connected") {
return { kind: "libCall", fn: "process.connected", args: [], type: BOOL, loc };
}
- // process.execArgv: the extra CLI arguments Node itself consumed — a
- // compiled binary consumed none, so the honest answer is a fresh [].
+ // process.execArgv: the Node runtime flags kept out of process.argv —
+ // a leading --test-update-snapshots in node:test programs, else [].
if (member === "execArgv") {
- return { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc };
+ return { kind: "libCall", fn: "process.execArgv", args: [], type: arrayOf(STRING), loc };
}
// process._exiting — the runtime's exit-sequence flag (true while
// 'exit' listeners run), Node's own undocumented member.
diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts
index 5fd189242..2dc212f81 100644
--- a/packages/compiler/src/frontend/lowering/builtins/promises.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts
@@ -279,25 +279,39 @@ export function lowerPromiseStaticCall(
// tick-for-tick in Node for plain results (probed), with the
// promise-returning form riding the async return-adoption machinery
// (its one-tick residue is SEMANTICS.md 358) — so the lowering IS
- // that wrapper: one interned async helper per callback type. The
- // ...args form fences (close over the values instead).
+ // that wrapper: one interned async helper per callback and argument
+ // types. `Promise.try(f, ...args)` evaluates the arguments at the call
+ // and passes them to f, which still runs synchronously.
if (member === "try") {
const fNode = call.arguments[0];
- if (call.arguments.length !== 1 || fNode === undefined || ts.isSpreadElement(fNode)) {
+ const argNodes = call.arguments.slice(1);
+ if (fNode === undefined || call.arguments.some(ts.isSpreadElement)) {
lowerer.noLowering(
- `Promise.try with ${call.arguments.length} arguments`,
+ `Promise.try with ${call.arguments.length === 0 ? "no" : "spread"} arguments`,
call,
- "the ...args form has no lowering — close over the values: Promise.try(() => f(a, b))",
+ "pass the callback and its arguments directly: Promise.try(f, a, b)",
);
}
const f = lowerer.lowerExpr(fNode);
- if (f.type.kind !== "func" || f.type.params.length !== 0) {
+ // Arguments past the callback's parameters are evaluated and dropped;
+ // only an arrow function, which has no `arguments`, can ignore them.
+ if (
+ f.type.kind !== "func" ||
+ f.type.params.length > argNodes.length ||
+ (f.type.params.length < argNodes.length && !ts.isArrowFunction(fNode))
+ ) {
lowerer.noLowering(
- `Promise.try over a callback of type '${lowerer.checker.typeToString(lowerer.typeOf(fNode))}'`,
+ `Promise.try over a callback of type '${lowerer.checker.typeToString(lowerer.typeOf(fNode))}' with ${argNodes.length} argument${argNodes.length === 1 ? "" : "s"}`,
fNode,
- "a zero-parameter function is the supported form",
+ "pass one argument per callback parameter: Promise.try((a, b) => a + b, 1, 2)",
);
}
+ const fnT = f.type;
+ const args = argNodes.map((node, i) =>
+ i < fnT.params.length
+ ? lowerer.lowerExprExpecting(node, fnT.params[i]!)
+ : lowerer.lowerExpr(node),
+ );
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (resultT?.kind !== "promise") {
lowerer.noLowering(
@@ -323,13 +337,22 @@ export function lowerPromiseStaticCall(
"annotate both to one T: Promise.try(f) with f returning T or Promise",
);
}
- const key = `promise.try:${typeKey(f.type)}`;
+ const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
helper = `%promise.try.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, helper);
const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc };
- const callF: IrExpr = { kind: "callValue", callee: fRef, args: [], type: ret, loc };
+ const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type }));
+ const callF: IrExpr = {
+ kind: "callValue",
+ callee: fRef,
+ args: argLocals
+ .slice(0, fnT.params.length)
+ .map((a) => ({ kind: "varRef" as const, localId: a.id, type: a.type, loc })),
+ type: ret,
+ loc,
+ };
// Adopt a promise result before either returning its value or completing void.
const value: IrExpr =
ret.kind === "promise" ? { kind: "awaitExpr", value: callF, type: ret.inner, loc } : callF;
@@ -342,16 +365,22 @@ export function lowerPromiseStaticCall(
: [{ kind: "return", value, loc }];
const lifted: IrFunction = {
name: helper,
- params: [{ localId: "f.0", name: "f", type: f.type }],
+ params: [
+ { localId: "f.0", name: "f", type: f.type },
+ ...argLocals.map((a) => ({ localId: a.id, name: a.name, type: a.type })),
+ ],
returnType: inner,
- locals: [{ id: "f.0", name: "f", type: f.type, mutable: true }],
+ locals: [
+ { id: "f.0", name: "f", type: f.type, mutable: true },
+ ...argLocals.map((a) => ({ ...a, mutable: true })),
+ ],
body,
loc,
async: true,
};
lowerer.liftedFns.push(lifted);
}
- return { kind: "call", callee: helper, args: [f], type: resultT, loc };
+ return { kind: "call", callee: helper, args: [f, ...args], type: resultT, loc };
}
// Promise.resolve — the already-settled promise. Zero arguments is
// Promise; a PROMISE argument is returned as-is (the spec's
diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts
index ab310fe52..98624dc4a 100644
--- a/packages/compiler/src/frontend/lowering/builtins/timers.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts
@@ -4,6 +4,7 @@ import { locOf } from "../../program.js";
import { generatorMeta } from "../call-signatures.js";
import {
BOOL,
+ DYN,
F64,
type IrExpr,
type IrFunction,
@@ -223,8 +224,8 @@ export function lowerTimeoutMethodCall(
// (scr_timer_coerce_ms clamps anyway; the literal keeps the emitted call
// self-describing). A resolve value travels as an already-resolved promise
// of its own type, whose settlement the result copies when it fires. The
-// options object takes `ref`; `signal` fences — a promise that ignored its
-// cancellation signal would hold the loop open where Node exits.
+// options object takes `ref` and `signal`; with a signal the runtime
+// clears the timer and rejects with an AbortError when it aborts.
export function lowerTimersPromisesCall(
lowerer: Lowerer,
expr: ts.CallExpression,
@@ -232,6 +233,8 @@ export function lowerTimersPromisesCall(
loc: SrcLoc,
): IrExpr | null {
const promiseVoid: IrType = { kind: "promise", inner: VOID };
+ if (bi.member === "scheduler.wait" || bi.member === "scheduler.yield")
+ return lowerSchedulerCall(lowerer, expr, bi.member, loc);
if (bi.member !== "setTimeout" && bi.member !== "setImmediate") return null;
const timeout = bi.member === "setTimeout";
const [valueNode, optionsNode] = timeout ? expr.arguments.slice(1) : expr.arguments;
@@ -273,7 +276,16 @@ export function lowerTimersPromisesCall(
type: resultT!,
loc,
};
- const ref = timersPromisesRef(lowerer, bi.member, optionsNode, loc);
+ const { ref, signal } = timersPromisesOptions(lowerer, bi.member, optionsNode, loc);
+ if (signal !== null) {
+ return {
+ kind: "libCall",
+ fn: timeout ? "tp.setTimeoutSignal" : "tp.setImmediateSignal",
+ args: timeout ? [ms!, value, ref, signal] : [value, ref, signal],
+ type: voidValue ? promiseVoid : resultT!,
+ loc,
+ };
+ }
return {
kind: "libCall",
fn: timeout ? "tp.setTimeoutValue" : "tp.setImmediateValue",
@@ -283,42 +295,85 @@ export function lowerTimersPromisesCall(
};
}
-/** The `ref` option of a timers/promises options object literal (true when
- * absent). `signal` and options that are not an object literal fence. */
-function timersPromisesRef(
+/** scheduler.wait(delay[, options]) and scheduler.yield(): Node's Scheduler
+ * methods return setTimeout(delay, undefined, options) and setImmediate(),
+ * so both settle a void promise from the same timer heap and immediate
+ * queue. The options object takes `ref` and `signal` as setTimeout's does. */
+function lowerSchedulerCall(
+ lowerer: Lowerer,
+ expr: ts.CallExpression,
+ member: "scheduler.wait" | "scheduler.yield",
+ loc: SrcLoc,
+): IrExpr {
+ const promiseVoid: IrType = { kind: "promise", inner: VOID };
+ if (member === "scheduler.yield")
+ return { kind: "libCall", fn: "tp.setImmediate", args: [], type: promiseVoid, loc };
+ // The scheduler.wait libCalls emit timers/promises setTimeout's runtime
+ // calls; mock.timers mocks scheduler.wait and leaves setTimeout's named
+ // and namespace imports real. An omitted delay is NaN, Node's undefined.
+ const [delayNode, optionsNode] = expr.arguments;
+ const ms: IrExpr = delayNode
+ ? lowerer.lowerExprExpecting(delayNode, F64)
+ : { kind: "numLit", value: NaN, type: F64, loc };
+ if (optionsNode === undefined)
+ return { kind: "libCall", fn: "tp.schedulerWait", args: [ms], type: promiseVoid, loc };
+ const { ref, signal } = timersPromisesOptions(lowerer, member, optionsNode, loc);
+ const value: IrExpr = {
+ kind: "intrinsic",
+ name: "promise.resolve",
+ args: [],
+ type: promiseVoid,
+ loc,
+ };
+ return {
+ kind: "libCall",
+ fn: signal === null ? "tp.schedulerWaitValue" : "tp.schedulerWaitSignal",
+ args: signal === null ? [ms, value, ref] : [ms, value, ref, signal],
+ type: promiseVoid,
+ loc,
+ };
+}
+
+/** The `ref` (true when absent) and `signal` (null when absent) options of
+ * a timers/promises options object literal, in source order. Options that
+ * are not an object literal fence. */
+function timersPromisesOptions(
lowerer: Lowerer,
member: string,
optionsNode: ts.Expression | undefined,
loc: SrcLoc,
-): IrExpr {
+): { ref: IrExpr; signal: IrExpr | null } {
const absent: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
- if (optionsNode === undefined) return absent;
+ if (optionsNode === undefined) return { ref: absent, signal: null };
if (!ts.isObjectLiteralExpression(optionsNode)) {
lowerer.noLowering(
`timers/promises ${member} options that are not an object literal`,
optionsNode,
- "pass the options as an object literal: { ref }",
+ "pass the options as an object literal: { ref, signal }",
);
- return absent;
+ return { ref: absent, signal: null };
}
let ref: IrExpr = absent;
+ let signal: IrExpr | null = null;
for (const prop of optionsNode.properties) {
const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null;
- if (name === "signal") {
- lowerer.noLowering(
- `timers/promises ${member} with an AbortSignal`,
- prop,
- "a promise that ignored its cancellation signal would keep the process alive where Node exits",
+ if (
+ name === "signal" &&
+ (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop))
+ ) {
+ signal = lowerer.lowerExprExpecting(
+ ts.isPropertyAssignment(prop) ? prop.initializer : (prop.name as ts.Identifier),
+ DYN,
);
} else if (name === "ref" && ts.isPropertyAssignment(prop)) {
ref = lowerer.lowerExprExpecting(prop.initializer, BOOL);
} else {
lowerer.noLowering(
- `timers/promises ${member} options other than ref`,
+ `timers/promises ${member} options other than ref and signal`,
prop,
- "the lowered options object takes ref: boolean",
+ "the lowered options object takes ref: boolean and signal: AbortSignal",
);
}
}
- return ref;
+ return { ref, signal };
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts
index 6e3beb9c2..daf857650 100644
--- a/packages/compiler/src/frontend/lowering/builtins/url.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/url.ts
@@ -26,6 +26,7 @@ import {
} from "../../../ir/ir.js";
import { optionalStringTags } from "./arguments.js";
import { type BuiltinModuleFn } from "../surfaces.js";
+import { lowerDynDispatchMethodCall } from "../lower-calls.js";
/** URL component assignment captures its receiver and RHS once, and yields
* the original RHS after the setter performs string conversion. */
@@ -255,10 +256,12 @@ export function lowerSearchParamsNew(
return { kind: "libCall", fn: "sp.fromPairs", args: [init], type: SEARCH_PARAMS_T, loc };
}
if (init.type.kind === "array" && init.type.elem.kind === "record") {
+ const pairs = searchParamsFromTuples(lowerer, init, init.type.elem, loc);
+ if (pairs) return pairs;
lowerer.unsupported(
"SC1090",
arg,
- "URLSearchParams from tuple-typed pairs (type the pairs as string[][] — the tuple rows have a different layout)",
+ "URLSearchParams from tuple rows other than [string, string] (type the pairs as [string, string][] or string[][])",
);
}
lowerer.unsupported(
@@ -268,6 +271,97 @@ export function lowerSearchParamsNew(
);
}
+/** `new URLSearchParams(pairs)` over a `[string, string][]` (an
+ * Object.entries result, for example): a helper per tuple shape appends
+ * each pair's name and value in array order. Null for other row types. */
+function searchParamsFromTuples(
+ lowerer: Lowerer,
+ init: IrExpr,
+ row: IrType & { kind: "record" },
+ loc: SrcLoc,
+): IrExpr | null {
+ const shape = lowerer.shapes.get(row.shapeId);
+ if (
+ !shape?.tuple ||
+ shape.fields.length !== 2 ||
+ !shape.fields.every(
+ (field) => (field.name === "0" || field.name === "1") && field.type.kind === "string",
+ )
+ )
+ return null;
+ const key = `sp.fromTuples:${row.shapeId}`;
+ let helper = lowerer.valueHelpers.get(key);
+ if (!helper) {
+ helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`;
+ lowerer.valueHelpers.set(key, helper);
+ const pairsT = init.type;
+ const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc);
+ const pair = (): IrExpr => varRef("pair.0", row, loc);
+ const field = (name: "0" | "1"): IrExpr => ({
+ kind: "recordGet",
+ obj: pair(),
+ shapeId: row.shapeId,
+ field: name,
+ type: STRING,
+ loc,
+ });
+ const body: IrStmt[] = [
+ {
+ kind: "varDecl",
+ localId: "sp.0",
+ init: { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc },
+ loc,
+ },
+ countedFor(
+ loc,
+ {
+ kind: "arrIntrinsic",
+ method: "length",
+ receiver: varRef("pairs.0", pairsT, loc),
+ args: [],
+ type: F64,
+ loc,
+ },
+ (index) => [
+ {
+ kind: "varDecl",
+ localId: "pair.0",
+ init: { kind: "arrayGet", arr: varRef("pairs.0", pairsT, loc), index, type: row, loc },
+ loc,
+ },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "sp.append",
+ args: [sp(), field("0"), field("1")],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ],
+ ),
+ { kind: "return", value: sp(), loc },
+ ];
+ const fn: IrFunction = {
+ name: helper,
+ params: [{ localId: "pairs.0", name: "pairs", type: pairsT }],
+ returnType: SEARCH_PARAMS_T,
+ locals: [
+ { id: "pairs.0", name: "pairs", type: pairsT, mutable: false },
+ { id: "sp.0", name: "sp", type: SEARCH_PARAMS_T, mutable: false },
+ { id: "i.0", name: "i", type: F64, mutable: true },
+ { id: "pair.0", name: "pair", type: row, mutable: false },
+ ],
+ body,
+ loc,
+ };
+ lowerer.liftedFns.push(fn);
+ }
+ return { kind: "call", callee: helper, args: [init], type: SEARCH_PARAMS_T, loc };
+}
+
function lowerOptionalStringSearchParams(
lowerer: Lowerer,
init: IrExpr,
@@ -534,12 +628,14 @@ export function lowerSearchParamsMethodCall(
if (name === "forEach" && args.length === 1) {
return lowerSpForEachCall(lowerer, call, access);
}
- if (name === "keys" || name === "values" || name === "entries") {
- lowerer.unsupported(
- "SC1090",
- call,
- `URLSearchParams iterator objects outside a for-of head (write \`for (const x of sp.${name}())\` directly)`,
- );
+ if ((name === "keys" || name === "values" || name === "entries") && args.length === 0) {
+ // Outside a for-of head the iterator is a live object: the checked-
+ // dynamic view's iterator walks the pair list by index
+ // (scr_native_sp_step), so entries appended after it was created are
+ // visited, as in Node.
+ const boxed = lowerer.coerceInto(access.expression, lowerer.lowerExpr(access.expression), DYN);
+ const iterator = lowerDynDispatchMethodCall(lowerer, call, access, boxed, false);
+ if (iterator) return iterator;
}
lowerer.noLowering(
`URLSearchParams.${name}`,
@@ -751,3 +847,21 @@ export function lowerLegacyUrlCall(
}
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
}
+
+/** A URLSearchParams value, or a keys()/values()/entries() call on one: an
+ * iterable whose checked-dynamic view walks the live pair list. */
+export function isSearchParamsIterable(lowerer: Lowerer, node: ts.Expression): boolean {
+ while (ts.isParenthesizedExpression(node)) node = node.expression;
+ if (lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "searchParams") return true;
+ if (
+ !ts.isCallExpression(node) ||
+ node.arguments.length !== 0 ||
+ !ts.isPropertyAccessExpression(node.expression)
+ )
+ return false;
+ const name = node.expression.name.text;
+ return (
+ (name === "keys" || name === "values" || name === "entries") &&
+ lowerer.mapTypeOf(lowerer.typeOf(node.expression.expression))?.kind === "searchParams"
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/util-types.ts b/packages/compiler/src/frontend/lowering/builtins/util-types.ts
index 979a996d8..4399c730e 100644
--- a/packages/compiler/src/frontend/lowering/builtins/util-types.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/util-types.ts
@@ -12,6 +12,22 @@ export function lowerUtilTypeCall(
): IrExpr {
if (expr.arguments.some(ts.isSpreadElement))
lowerer.noLowering(`${name} with spread arguments`, expr);
+ const first = expr.arguments[0];
+ if (first !== undefined && lowerer.mapTypeOf(lowerer.typeOf(first))?.kind === "date") {
+ // A Date value has no checked-dynamic form; its static type decides
+ // every brand. The arguments still evaluate in order.
+ return {
+ kind: "seqExpr",
+ stmts: expr.arguments.map((arg): IrStmt => ({
+ kind: "exprStmt",
+ expr: lowerer.lowerExpr(arg),
+ loc,
+ })),
+ result: { kind: "boolLit", value: bi.member === "isDate", type: BOOL, loc },
+ type: BOOL,
+ loc,
+ };
+ }
const input = expr.arguments[0]
? lowerer.lowerExprExpecting(expr.arguments[0], DYN)
: dynUndefinedExpr(loc);
diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts
index 3144f208f..2311e0f96 100644
--- a/packages/compiler/src/frontend/lowering/builtins/workers.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts
@@ -9,6 +9,7 @@ import {
F64,
STRING,
UNDEFINED_T,
+ VOID,
isFfiCallbackParam,
type IrExpr,
type SrcLoc,
@@ -27,6 +28,10 @@ export function lowerWorkerMetadata(member: string, loc: SrcLoc): IrExpr | null
return { kind: "libCall", fn: "worker.parentPort", args: [], type: DYN, loc };
case "isInternalThread":
return { kind: "boolLit", value: false, type: BOOL, loc };
+ case "resourceLimits":
+ return { kind: "libCall", fn: "worker.resourceLimits", args: [], type: DYN, loc };
+ case "threadName":
+ return { kind: "libCall", fn: "worker.threadName", args: [], type: DYN, loc };
default:
return null;
}
@@ -76,6 +81,7 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression):
"native callbacks in worker executables must run on their owning script thread",
);
}
+ refuseHeapLimits(lowerer, args[1]);
lowerer.usesWorkers = true;
const loc = locOf(expression);
const options: IrExpr = args[1]
@@ -86,7 +92,7 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression):
type: DYN,
loc,
};
- return {
+ const create: IrExpr = {
kind: "libCall",
fn: "worker.new",
args: [
@@ -97,6 +103,70 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression):
type: DYN,
loc,
};
+ if (!capturesStdio(args[1])) return create;
+ return {
+ kind: "seqExpr",
+ stmts: [
+ {
+ kind: "exprStmt",
+ expr: { kind: "libCall", fn: "stream.workerStdio", args: [], type: VOID, loc },
+ loc,
+ },
+ ],
+ result: create,
+ type: DYN,
+ loc,
+ };
+}
+
+/** True when the options are not a literal, or a literal whose stdout,
+ * stderr or stdin member is not the literal false: worker.stdout,
+ * worker.stderr and worker.stdin then need the stream unit. */
+function capturesStdio(options: ts.Expression | undefined): boolean {
+ if (!options) return false;
+ if (!ts.isObjectLiteralExpression(options)) return true;
+ return options.properties.some((option) => {
+ if (ts.isSpreadAssignment(option)) return true;
+ if (!option.name) return false;
+ const name = propertyNameText(option.name);
+ if (name !== "stdout" && name !== "stderr" && name !== "stdin") return name === undefined;
+ return !(
+ ts.isPropertyAssignment(option) && option.initializer.kind === ts.SyntaxKind.FalseKeyword
+ );
+ });
+}
+
+const HEAP_LIMITS = new Set([
+ "maxOldGenerationSizeMb",
+ "maxYoungGenerationSizeMb",
+ "codeRangeSizeMb",
+]);
+
+/** resourceLimits' V8 heap fields, when an options literal spells them
+ * with numbers (Node ignores other values; the runtime refuses the rest). */
+function refuseHeapLimits(lowerer: Lowerer, options: ts.Expression | undefined): void {
+ if (!options || !ts.isObjectLiteralExpression(options)) return;
+ for (const option of options.properties) {
+ if (!ts.isPropertyAssignment(option) || propertyNameText(option.name) !== "resourceLimits")
+ continue;
+ if (!ts.isObjectLiteralExpression(option.initializer)) continue;
+ for (const limit of option.initializer.properties) {
+ if (!ts.isPropertyAssignment(limit)) continue;
+ const name = propertyNameText(limit.name);
+ if (name === undefined || !HEAP_LIMITS.has(name)) continue;
+ const type = lowerer.checker.getTypeAtLocation(limit.initializer);
+ if (!(type.flags & ts.TypeFlags.NumberLike)) continue;
+ lowerer.noLowering(
+ `Worker resourceLimits.${name}`,
+ limit,
+ "worker threads have no separate V8 heap; resourceLimits supports stackSizeMb",
+ );
+ }
+ }
+}
+
+function propertyNameText(name: ts.PropertyName): string | undefined {
+ return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
}
/** True when `node`'s type is worker_threads' `name` class (also reached
@@ -166,3 +236,142 @@ export function lowerReceiveMessageOnPort(lowerer: Lowerer, call: ts.CallExpress
loc,
};
}
+
+/** new BroadcastChannel(name): the EventTarget unit's half installs
+ * first, then the channel opens (the name converts at runtime). */
+export function lowerBroadcastChannelNew(
+ lowerer: Lowerer,
+ expression: ts.NewExpression,
+): IrExpr | null {
+ if (!isWorkerThreadsClass(lowerer, expression, "BroadcastChannel")) return null;
+ const args = expression.arguments ?? [];
+ if (args.length > 1 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(
+ `BroadcastChannel constructor with ${args.length} arguments`,
+ expression,
+ "use new BroadcastChannel(name)",
+ );
+ }
+ if (lowerer.targetPlatform === "wasi") {
+ lowerer.noLowering(
+ "BroadcastChannel in this target",
+ expression,
+ "use a native static executable",
+ );
+ }
+ lowerer.usesWorkers = true;
+ const loc = locOf(expression);
+ return {
+ kind: "seqExpr",
+ stmts: [
+ {
+ kind: "exprStmt",
+ expr: { kind: "libCall", fn: "event.broadcastInstall", args: [], type: VOID, loc },
+ loc,
+ },
+ ],
+ result: {
+ kind: "libCall",
+ fn: "worker.broadcastChannel",
+ args: [
+ {
+ kind: "dynArrLit",
+ elems: args.map((a) => lowerer.lowerExprExpecting(a, DYN)),
+ type: DYN,
+ loc,
+ },
+ ],
+ type: DYN,
+ loc,
+ },
+ type: DYN,
+ loc,
+ };
+}
+
+/** setEnvironmentData(key, value?) and getEnvironmentData(key): this
+ * thread's map, cloned into each Worker it constructs. */
+export function lowerEnvironmentData(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ member: "setEnvironmentData" | "getEnvironmentData",
+): IrExpr {
+ const args = call.arguments;
+ const arity = member === "setEnvironmentData" ? 2 : 1;
+ if (args.length > arity || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(`${member} with ${args.length} arguments`, call);
+ }
+ if (lowerer.targetPlatform === "wasi") {
+ lowerer.noLowering(`${member} in this target`, call, "use a native static executable");
+ }
+ lowerer.usesWorkers = true;
+ const loc = locOf(call);
+ const lowered: IrExpr[] = [];
+ for (let i = 0; i < arity; i++) {
+ lowered.push(
+ args[i]
+ ? lowerer.lowerExprExpecting(args[i]!, DYN)
+ : {
+ kind: "dynFrom",
+ value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
+ type: DYN,
+ loc,
+ },
+ );
+ }
+ return {
+ kind: "libCall",
+ fn: member === "setEnvironmentData" ? "worker.setEnvironmentData" : "worker.getEnvironmentData",
+ args: lowered,
+ type: member === "setEnvironmentData" ? VOID : DYN,
+ loc,
+ };
+}
+
+/** markAsUncloneable(value): structuredClone and postMessage then throw
+ * DataCloneError for the value (scr_json.c). It needs no worker runtime. */
+export function lowerUncloneable(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
+ const args = call.arguments;
+ if (args.length > 1 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(`markAsUncloneable with ${args.length} arguments`, call);
+ }
+ const loc = locOf(call);
+ const value: IrExpr = args[0]
+ ? lowerer.lowerExprExpecting(args[0], DYN)
+ : {
+ kind: "dynFrom",
+ value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
+ type: DYN,
+ loc,
+ };
+ return { kind: "libCall", fn: "dyn.markUncloneable", args: [value], type: VOID, loc };
+}
+
+/** markAsUntransferable(value) and isMarkedAsUntransferable(value): marks
+ * kept by this thread (an ArrayBuffer's on its storage). */
+export function lowerUntransferable(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ member: "markAsUntransferable" | "isMarkedAsUntransferable",
+): IrExpr {
+ const args = call.arguments;
+ if (args.length > 1 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(`${member} with ${args.length} arguments`, call);
+ }
+ if (lowerer.targetPlatform === "wasi") {
+ lowerer.noLowering(`${member} in this target`, call, "use a native static executable");
+ }
+ lowerer.usesWorkers = true;
+ const loc = locOf(call);
+ const value: IrExpr = args[0]
+ ? lowerer.lowerExprExpecting(args[0], DYN)
+ : {
+ kind: "dynFrom",
+ value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
+ type: DYN,
+ loc,
+ };
+ return member === "markAsUntransferable"
+ ? { kind: "libCall", fn: "worker.markUntransferable", args: [value], type: VOID, loc }
+ : { kind: "libCall", fn: "worker.isMarkedUntransferable", args: [value], type: BOOL, loc };
+}
diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts
index 0499223f1..8e30e8b82 100644
--- a/packages/compiler/src/frontend/lowering/call-signatures.ts
+++ b/packages/compiler/src/frontend/lowering/call-signatures.ts
@@ -123,6 +123,24 @@ export function generatorMeta(
return { yieldT: type.yieldT, nextT: type.nextT, resultType };
}
+/** A parameter with a type annotation or a JSDoc `@param`/`@type` tag. */
+export function documentedParameter(param: ts.ParameterDeclaration): boolean {
+ return (
+ param.type !== undefined ||
+ /@(?:param|type)\b/.test(
+ param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
+ )
+ );
+}
+
+/** A documented parameter in a JavaScript file TypeScript checks. */
+export function checkedJsDocumentedParameter(
+ lowerer: Lowerer,
+ param: ts.ParameterDeclaration,
+): boolean {
+ return lowerer.checkedJsFile(param.getSourceFile()) && documentedParameter(param);
+}
+
/** One parameter's ParamShape — the shared signature-shaped collection
* point for function declarations, methods, constructors, and lambdas
* (generic declarations defer to their call sites, where the resolved
@@ -147,11 +165,14 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
if (
isJsSourceFile(param.getSourceFile()) &&
ts.isConstructorDeclaration(param.parent) &&
- !param.dotDotDotToken
+ !param.dotDotDotToken &&
+ !checkedJsDocumentedParameter(lowerer, param)
) {
// Reflective JavaScript construction can supply values outside the
// constructor's documented usual types. Retain the actual arguments
// and apply checked conversions only at operations that need them.
+ // In a file TypeScript checks, documented types are enforced like
+ // TypeScript annotations.
return param.initializer
? { type: DYN, mode: "omittable", bodyType: DYN }
: { type: DYN, mode: param.questionToken ? "omittable" : "required" };
@@ -722,7 +743,7 @@ export function lambdaSignature(
return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) };
}
}
- if (node.typeParameters) {
+ if (node.typeParameters && !lowerer.erasingLambdas.has(node)) {
// Generic function-like forms monomorphize only where a static home
// exists: top-level generic function declarations, generic methods
// (class and object-literal), and module-scope never-reassigned
diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts
index da40f361b..859938845 100644
--- a/packages/compiler/src/frontend/lowering/class-callbacks.ts
+++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts
@@ -14,12 +14,14 @@ import { isJsSourceFile, locOf } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import { findMethodOn, type ClassInfo } from "./lower-classes.js";
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
-import { classPrototypeData } from "./class-prototypes.js";
+import { observedPrototypeMethod } from "./class-prototypes.js";
import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
/** Unmodified typed methods retain direct or virtual native dispatch.
- * JavaScript notification methods keep their checked property fallback. */
+ * JavaScript notification methods keep their checked property fallback,
+ * except in files TypeScript checks, whose methods join the method census
+ * like TypeScript methods. */
export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
if (!isClassOwnEnumerableFieldName(name) || name.startsWith("get:") || name.startsWith("set:"))
return false;
@@ -38,11 +40,22 @@ export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string)
(lowerer.prototypeMethodAccesses.has(name) ||
(info.decl !== null &&
isJsSourceFile(info.decl.getSourceFile()) &&
+ !lowerer.checkedJsFile(info.decl.getSourceFile()) &&
found.sig.params.length === 0 &&
found.sig.ret.kind === "void"))
);
}
+/** The census marked `name` and the receiver's class keeps the method in
+ * its prototype data, so the property bag answers every read. */
+function observedSlot(lowerer: Lowerer, receiver: IrExpr, name: string, loc: SrcLoc): boolean {
+ const info =
+ receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined;
+ return info
+ ? observedPrototypeMethod(lowerer, info, name, loc, receiver)
+ : lowerer.prototypeMethodAccesses.has(name);
+}
+
function callbackBag(
lowerer: Lowerer,
receiver: IrExpr,
@@ -50,9 +63,8 @@ function callbackBag(
name: string,
readOnly = false,
): IrExpr {
- const info =
- receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : null;
- if (info && lowerer.prototypeMethodAccesses.has(name)) classPrototypeData(lowerer, info, loc);
+ // Builds the prototype data that an observed read falls back to.
+ observedSlot(lowerer, receiver, name, loc);
return {
kind: "call",
callee: classPropertiesHelper(lowerer, loc, readOnly).name,
@@ -116,7 +128,7 @@ export function classCallbackValue(
const bag = lowerer.declareHiddenLocal("%callbackBag", DYN);
const value = varRef(bag.id, DYN, loc);
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
- const result: IrExpr = lowerer.prototypeMethodAccesses.has(name)
+ const result: IrExpr = observedSlot(lowerer, receiver, name, loc)
? { kind: "dynKeyGet", value, key, type: DYN, loc }
: {
kind: "ternary",
@@ -173,7 +185,8 @@ export function classCallbackCall(
type: DYN,
loc,
};
- const result: IrExpr = lowerer.prototypeMethodAccesses.has(name)
+ const observed = observedSlot(lowerer, receiver, name, loc);
+ const result: IrExpr = observed
? call
: {
kind: "ternary",
@@ -206,7 +219,7 @@ export function classCallbackCall(
{
kind: "varDecl",
localId: present.id,
- init: lowerer.prototypeMethodAccesses.has(name)
+ init: observed
? { kind: "boolLit", value: true, type: BOOL, loc }
: callbackPresent(value, key, loc),
loc,
diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
index 102ab79d1..444a9cf23 100644
--- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
@@ -1,4 +1,4 @@
-import { dynUndefinedExpr, varRef } from "../../ir/build.js";
+import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
import {
BOOL,
@@ -24,7 +24,7 @@ import {
import { streamTypedRefEligible } from "../../ir/analysis.js";
import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js";
import { PoisonError, type Lowerer } from "./lowerer.js";
-import { implicitDefaultInstance } from "./generic-functions.js";
+import { erasedGenericInstance, implicitDefaultInstance } from "./generic-functions.js";
import { type ParamShape } from "./call-signatures.js";
import {
accessorCall,
@@ -40,6 +40,7 @@ import {
import {
classPrototypeData,
hasClassPrototypeData,
+ observedPrototypeMethod,
reflectClassPrototype,
} from "./class-prototypes.js";
import {
@@ -67,6 +68,7 @@ import { ClassConstructionDispatch } from "./class-construction.js";
import { emitterRooted } from "./lower-event-emitter.js";
import { protocolArm, protocolFieldArm } from "./class-protocol-dispatch.js";
import { STREAM_OPERATORS } from "./lower-stream.js";
+import { deletedPropertyNames, fixedFieldFamily } from "./js-class-fields.js";
const STREAM_METHODS = new Set([
"on",
@@ -157,6 +159,81 @@ interface Dispatch {
classes: Set;
}
+type ToPrimitiveHint = "default" | "number" | "string";
+type ConversionMethod = "valueOf" | "toString" | "sym:toPrimitive";
+const CONVERSION_CALL_PREFIX = "%conversionCall.";
+type Hoisted = { stmts: IrStmt[]; value: IrExpr };
+const TO_PRIMITIVE_PREFIX = "%dyn.class.toPrimitive.";
+
+/** Operations that convert their dynamic operands with ToPrimitive: `+`
+ * and `==` take the default hint, the arithmetic, bitwise, relational and
+ * numeric conversions the number hint, and the string conversions the
+ * string hint. */
+const DEFAULT_HINT_CALLS = new Set(["dyn.abstractEq", "dyn.add"]);
+const NUMBER_HINT_CALLS = new Set([
+ "dyn.arithmetic",
+ "dyn.bigintConstructor",
+ "dyn.bitwise",
+ "dyn.compare",
+ "dyn.numberConstructor",
+ "dyn.toNumberCoerce",
+ "dyn.toNumeric",
+]);
+const STRING_HINT_CALLS = new Set(["dyn.stringConstructor", "dyn.toStringCoerce"]);
+/** Operations whose second operand is a property key, converted with the
+ * string hint when the receiver admits a key: `in` needs an object,
+ * propertyIsEnumerable converts before checking its receiver, and the
+ * others need a receiver other than null and undefined. */
+const KEY_CALLS = new Map([
+ ["dyn.hasKeyComputed", "object"],
+ ["dyn.propertyIsEnumerableComputed", "any"],
+ ["dyn.hasOwnComputed", "value"],
+ ["dyn.keyDeleteComputed", "value"],
+ ["dyn.keySetComputed", "value"],
+]);
+
+function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null {
+ if (expr.kind === "toString") return expr.operand.type.kind === "dyn" ? "string" : null;
+ if (expr.kind === "dynKeyGet") return expr.key.type.kind === "dyn" ? "string" : null;
+ if (expr.kind !== "libCall") return null;
+ if (DEFAULT_HINT_CALLS.has(expr.fn)) return "default";
+ if (NUMBER_HINT_CALLS.has(expr.fn)) return "number";
+ if (STRING_HINT_CALLS.has(expr.fn) || KEY_CALLS.has(expr.fn)) return "string";
+ return null;
+}
+
+/** An operand that needs no conversion: already passed through a
+ * conversion helper (loose equality converts conditionally), or boxed from
+ * a statically primitive value. */
+function isConversion(expr: IrExpr): boolean {
+ if (expr.kind === "ternary") return isConversion(expr.then);
+ if (expr.kind === "dynFrom") return PRIMITIVE_KINDS.has(expr.value.type.kind);
+ return expr.kind === "call" && expr.callee.startsWith(TO_PRIMITIVE_PREFIX);
+}
+
+const PRIMITIVE_KINDS = new Set([
+ "f64",
+ "string",
+ "bool",
+ "bigint",
+ "symbol",
+ "undefinedT",
+ "nullT",
+]);
+
+/** A declared conversion method, own or inherited. */
+function conversionMethod(lowerer: Lowerer, info: ClassInfo, method: ConversionMethod): boolean {
+ if (info.fields.has(method)) return false;
+ const found = findMethodOn(lowerer, info, method);
+ return !!found && !found.declarer.builtinError;
+}
+
+function isPrimitive(value: IrExpr, loc: SrcLoc): IrExpr {
+ return (["number", "string", "boolean", "undefined", "null", "bigint", "symbol"] as const)
+ .map((test): IrExpr => ({ kind: "dynTest", test, value, type: BOOL, loc }))
+ .reduce((a, b) => ({ kind: "logical", op: "||", left: a, right: b, type: BOOL, loc }));
+}
+
interface PropertyReceiver {
info: ClassInfo;
capsule: ClassInfo;
@@ -594,6 +671,9 @@ export class ClassDynamicDispatch {
private readonly generatorDispatch = new GeneratorDynamicDispatch();
private readonly caughtDispatch = new CaughtValueDispatch();
private readonly constructionDispatch = new ClassConstructionDispatch();
+ /** Property names a delete can remove (null: any name), for the field
+ * creation tracking decision. */
+ private deletedNames: Set | null | undefined;
private readonly boxed = new Set();
private readonly boxedConstructors = new Set();
private readonly dispatches = new Map();
@@ -618,6 +698,8 @@ export class ClassDynamicDispatch {
private readonly bagClasses = new Set();
private readonly bagInitializers = new Map[]>();
private readonly typedPropertyBags = new Map();
+ /** Per-class prototype selection for new property bags (bagPrototypeHelper). */
+ private readonly bagPrototypeHelpers = new Map();
private readonly readPropertyBags: {
helper: IrFunction;
initialize: Extract;
@@ -641,6 +723,19 @@ export class ClassDynamicDispatch {
/** The for-in key helpers, by libCall (dyn and native string[] keys). */
private readonly enumerations = new Map<"dyn.forInKeys" | "dyn.forInKeyStrings", IrFunction>();
private readonly enumerationClasses = new Set();
+ /** OrdinaryToPrimitive over boxed class instances, by hint, and the
+ * per-class method calls its arms make. */
+ private readonly toPrimitives = new Map<
+ ToPrimitiveHint,
+ { fn: IrFunction; classes: Set }
+ >();
+ private readonly toPrimitiveCalls = new Map();
+ /** `value.valueOf()` and `value.toString()` on a dynamic receiver: a test
+ * for boxed instances of classes declaring the method, and the call. */
+ private readonly conversionCalls = new Map<
+ "valueOf" | "toString",
+ { test: IrFunction; call: IrFunction; classes: Set }
+ >();
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
// Dispatch tables grow between worklist waves. Keep their normalization
@@ -662,6 +757,8 @@ export class ClassDynamicDispatch {
changed = this.constructionDispatch.process(lowerer, functions) || changed;
const seenTypes = new Set();
const rewrite = new Set();
+ const toPrimitiveSites = new Set();
+ const hasKeySites = new Set();
const discover = (type: IrType): void => {
const key = typeKey(type);
if (seenTypes.has(key)) return;
@@ -699,6 +796,11 @@ export class ClassDynamicDispatch {
) {
this.serializesClasses = true;
}
+ if (
+ toPrimitiveHint(expr) !== null ||
+ (expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString"))
+ )
+ toPrimitiveSites.add(fn);
switch (expr.kind) {
case "dynFrom":
discover(expr.value.type);
@@ -719,6 +821,9 @@ export class ClassDynamicDispatch {
)
rewrite.add(fn);
break;
+ case "dynHasKey":
+ hasKeySites.add(fn);
+ break;
case "dynKeyGet":
case "dynInvoke":
rewrite.add(fn);
@@ -752,6 +857,11 @@ export class ClassDynamicDispatch {
},
});
if (this.boxed.size === 0 && rewrite.size === 0) return changed;
+ const converting = this.convertingHints(lowerer);
+ const calling = this.calledConversions(lowerer);
+ if (converting.size || calling.size) for (const fn of toPrimitiveSites) rewrite.add(fn);
+ const prototypeMembers = this.prototypeMemberNames(lowerer);
+ if (prototypeMembers.size) for (const fn of hasKeySites) rewrite.add(fn);
for (const fn of functions) {
if (
!fn.name.startsWith("%dyn.class.instanceof:") &&
@@ -1019,38 +1129,8 @@ export class ClassDynamicDispatch {
current.subclasses.some(
(child) => lowerer.classCanBeConstructed(child) && needsPrototype(child),
);
- const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => {
- const currentReceiver: IrExpr =
- receiver.type.kind === "object" && receiver.type.className !== current.def.name
- ? {
- kind: "downcast",
- value: receiver,
- type: { kind: "object", className: current.def.name },
- loc,
- }
- : receiver;
- let result = classPrototypeData(lowerer, current, loc, currentReceiver);
- for (const child of current.subclasses) {
- if (!lowerer.classCanBeConstructed(child)) continue;
- const selected = prototypeFor(child, receiver, loc);
- if (selected && result)
- result = {
- kind: "ternary",
- cond: {
- kind: "instanceOf",
- value: receiver,
- className: child.def.name,
- type: BOOL,
- loc,
- },
- then: selected,
- else_: result,
- type: DYN,
- loc,
- };
- }
- return result;
- };
+ const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null =>
+ this.bagPrototype(lowerer, current, receiver, loc);
for (const existingInit of this.bagInitializers.get(className) ?? []) {
if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) {
const prototype = prototypeFor(info, existingInit.obj, existingInit.loc);
@@ -1076,8 +1156,11 @@ export class ClassDynamicDispatch {
current.def.tracksOwnFields = true;
current.subclasses.forEach(track);
};
- // Runtime-owned roots keep their dedicated property contracts.
- if (!root.base?.def.runtime) track(root);
+ // Runtime-owned roots keep their dedicated property contracts. A
+ // family whose fields all exist from construction and are never
+ // deleted keeps a fixed layout, as TypeScript classes do.
+ this.deletedNames ??= deletedPropertyNames(lowerer.moduleOrder);
+ if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
}
const loc = this.propertyBag.loc;
const { receiver, condition } = this.propertyReceiver(plan, loc);
@@ -1189,6 +1272,53 @@ export class ClassDynamicDispatch {
fn.body = transformStmtList(fn.body, {
stmt: (stmt) => stmt,
expr: (expr) => {
+ if (expr.kind === "dynHasKey" && prototypeMembers.has(expr.key)) {
+ // A class instance's snapshot can carry its class's prototype
+ // data, which holds only the methods the program reads through
+ // the prototype. The computed-key form answers methods and
+ // accessors from the class itself.
+ const loc = expr.loc;
+ const test: IrExpr = {
+ kind: "libCall",
+ fn: "dyn.hasKeyComputed",
+ args: [
+ expr.value,
+ {
+ kind: "dynFrom",
+ value: { kind: "strLit", value: expr.key, type: STRING, loc },
+ type: DYN,
+ loc,
+ },
+ ],
+ type: BOOL,
+ loc,
+ };
+ changed = true;
+ return {
+ kind: "seqExpr",
+ stmts: [],
+ result: expr.negated
+ ? { kind: "unary", op: "!", operand: test, type: BOOL, loc }
+ : test,
+ type: BOOL,
+ loc,
+ };
+ }
+ const called = calling.size ? this.convertMethodCall(lowerer, fn, expr, calling) : null;
+ if (called) {
+ changed = true;
+ return called;
+ }
+ const converted = converting.size
+ ? this.convertOperands(lowerer, fn, expr, converting)
+ : null;
+ if (converted) {
+ changed = true;
+ // A hoisting sequence revisits the operation as its child; a
+ // wrapped operation continues through the rewrites below.
+ if (converted.kind === "seqExpr") return converted;
+ expr = converted;
+ }
if (expr.kind === "dynCheck" && isDynTypedRefType(expr.type)) {
const type = expr.type;
let cast = this.checkedCasts.get(type.className);
@@ -2319,8 +2449,13 @@ export class ClassDynamicDispatch {
const value = varRef("p.0", DYN, loc);
const { receiver, condition } = this.propertyReceiver(plan, loc);
if (isClassCallback(lowerer, info, dispatch.source.method)) {
- const prototypeMethod = lowerer.prototypeMethodAccesses.has(dispatch.source.method);
- if (prototypeMethod) classPrototypeData(lowerer, info, loc);
+ const prototypeMethod = observedPrototypeMethod(
+ lowerer,
+ info,
+ dispatch.source.method,
+ loc,
+ receiver,
+ );
const bag = this.readPropertyBagOrEmpty(lowerer, value, loc);
dispatch.callback.body.unshift({
kind: "if",
@@ -2390,6 +2525,57 @@ export class ClassDynamicDispatch {
changed = true;
}
}
+ for (const [method, conversion] of this.conversionCalls) {
+ for (const plan of this.propertyReceivers(lowerer)) {
+ if (conversion.classes.has(plan.key)) continue;
+ conversion.classes.add(plan.key);
+ if (!conversionMethod(lowerer, plan.info, method)) continue;
+ const loc = conversion.call.loc;
+ const { condition } = this.propertyReceiver(plan, loc);
+ const value = varRef("p.0", DYN, loc);
+ const result: IrExpr = {
+ kind: "call",
+ callee: this.toPrimitiveCall(lowerer, plan, method, method === "toString" ? 1 : 0, loc),
+ args: method === "toString" ? [value, varRef("p.1", DYN, loc)] : [value],
+ type: DYN,
+ loc,
+ };
+ conversion.test.body.unshift({
+ kind: "if",
+ cond: condition,
+ then: [{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }],
+ else_: null,
+ loc,
+ });
+ conversion.call.body.unshift({
+ kind: "if",
+ cond: condition,
+ then: [
+ {
+ kind: "return",
+ value:
+ method === "toString"
+ ? { kind: "dynCheck", value: result, type: STRING, loc }
+ : result,
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ });
+ changed = true;
+ }
+ }
+ for (const [hint, conversion] of this.toPrimitives) {
+ for (const plan of this.propertyReceivers(lowerer)) {
+ if (conversion.classes.has(plan.key)) continue;
+ conversion.classes.add(plan.key);
+ const arm = this.toPrimitiveArm(lowerer, plan, hint, conversion.fn);
+ if (!arm) continue;
+ conversion.fn.body.unshift(arm);
+ changed = true;
+ }
+ }
for (const dispatch of lowerer.protocolDispatches.values()) {
if (!this.generated.has(dispatch.fn)) {
this.generated.add(dispatch.fn);
@@ -2616,7 +2802,8 @@ export class ClassDynamicDispatch {
return dispatch.reflect
? this.reflectPropertyBody(lowerer, { ...dispatch, name }, info, receiver)
: this.propertyBody(lowerer, { ...dispatch, name }, info, receiver);
- const prototypeMethod = lowerer.prototypeMethodAccesses.has(name);
+ const prototypeMethod =
+ !info.fields.has(name) && observedPrototypeMethod(lowerer, info, name, loc, receiver);
if (info.fields.has(name) || (dispatch.probe === "in" && !prototypeMethod)) {
if (info.fields.has(name) && info.def.tracksOwnFields) {
const bag = this.readPropertyBagOrEmpty(
@@ -2649,7 +2836,6 @@ export class ClassDynamicDispatch {
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
];
}
- if (prototypeMethod) classPrototypeData(lowerer, info, loc, receiver);
const bag = this.readPropertyBagOrEmpty(
lowerer,
lowerer.coerceToExpected(receiver, DYN),
@@ -3105,6 +3291,555 @@ export class ClassDynamicDispatch {
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
}
+ /** The hints for which some boxed class declares a conversion method. A
+ * string conversion calls toString first, so without a declared toString
+ * or Symbol.toPrimitive it answers Object.prototype.toString's
+ * "[object Object]" as before. */
+ private convertingHints(lowerer: Lowerer): Set {
+ const hints = new Set();
+ for (const { info } of this.propertyReceivers(lowerer)) {
+ if (
+ conversionMethod(lowerer, info, "sym:toPrimitive") ||
+ conversionMethod(lowerer, info, "toString")
+ ) {
+ hints.add("string");
+ hints.add("number");
+ hints.add("default");
+ } else if (conversionMethod(lowerer, info, "valueOf")) {
+ hints.add("number");
+ hints.add("default");
+ }
+ }
+ return hints;
+ }
+
+ /** An operation that converts dynamic operands to primitives, with each
+ * operand passed through the hint's conversion helper first. Operations
+ * with two dynamic operands evaluate both before converting the left one,
+ * as JavaScript does. Returns null when `expr` needs no conversion. */
+ private convertOperands(
+ lowerer: Lowerer,
+ fn: IrFunction,
+ expr: IrExpr,
+ converting: Set,
+ ): IrExpr | null {
+ const hint = toPrimitiveHint(expr);
+ if (hint === null || !converting.has(hint)) return null;
+ const loc = expr.loc;
+ const convert = (value: IrExpr): IrExpr => ({
+ kind: "call",
+ callee: this.toPrimitiveHelper(lowerer, hint, loc),
+ args: [value],
+ type: DYN,
+ loc,
+ });
+ if (expr.kind === "toString") {
+ if (isConversion(expr.operand)) return null;
+ return { ...expr, operand: convert(expr.operand) };
+ }
+ if (expr.kind === "dynKeyGet") {
+ // A computed read converts its key after evaluating the receiver,
+ // unless the receiver is null or undefined.
+ if (expr.key.type.kind !== "dyn" || isConversion(expr.key)) return null;
+ const [receiver, key] = this.hoistOperands(fn, [expr.value, expr.key], loc) as [
+ Hoisted,
+ Hoisted,
+ ];
+ return {
+ kind: "seqExpr",
+ stmts: receiver.stmts.concat(key.stmts),
+ result: {
+ ...expr,
+ value: receiver.value,
+ key: {
+ kind: "ternary",
+ cond: {
+ kind: "dynTest",
+ test: "nullish",
+ negated: true,
+ value: receiver.value,
+ type: BOOL,
+ loc,
+ },
+ then: convert(key.value),
+ else_: key.value,
+ type: DYN,
+ loc,
+ },
+ },
+ type: expr.type,
+ loc,
+ };
+ }
+ if (expr.kind !== "libCall") return null;
+ const receiverNeeds = KEY_CALLS.get(expr.fn);
+ if (receiverNeeds) {
+ if (expr.args[1]?.type.kind !== "dyn" || isConversion(expr.args[1])) return null;
+ // Every operand is evaluated before the key converts, as in
+ // `obj[key] = value`, where the value is evaluated first.
+ const hoisted = this.hoistOperands(fn, expr.args, loc);
+ const args = hoisted.map((operand) => operand.value);
+ const receiver = args[0]!;
+ const admits: IrExpr | null =
+ receiverNeeds === "any"
+ ? null
+ : receiverNeeds === "object"
+ ? { kind: "unary", op: "!", operand: isPrimitive(receiver, loc), type: BOOL, loc }
+ : { kind: "dynTest", test: "nullish", negated: true, value: receiver, type: BOOL, loc };
+ args[1] = admits
+ ? {
+ kind: "ternary",
+ cond: admits,
+ then: convert(args[1]!),
+ else_: args[1]!,
+ type: DYN,
+ loc,
+ }
+ : convert(args[1]!);
+ return {
+ kind: "seqExpr",
+ stmts: hoisted.flatMap((operand) => operand.stmts),
+ result: { ...expr, args },
+ type: expr.type,
+ loc,
+ };
+ }
+ const dynamic = expr.args.flatMap((arg, i) => (arg.type.kind === "dyn" ? [i] : []));
+ if (dynamic.some((i) => isConversion(expr.args[i]!) && expr.args[i]!.kind !== "dynFrom"))
+ return null;
+ const operands = dynamic.filter((i) => !isConversion(expr.args[i]!));
+ if (operands.length === 0) return null;
+ if (expr.args.length === 1) return { ...expr, args: [convert(expr.args[0]!)] };
+ // Every operand is evaluated before the first conversion.
+ const hoisted = expr.args.map((arg): Hoisted =>
+ arg.kind === "strLit" || arg.kind === "numLit" || arg.kind === "boolLit"
+ ? { stmts: [], value: arg }
+ : this.hoistOperands(fn, [arg], loc)[0]!,
+ );
+ const args = hoisted.map((operand) => operand.value);
+ if (expr.fn === "dyn.abstractEq") {
+ // Loose equality converts an object only when the other operand is a
+ // primitive other than null and undefined.
+ if (dynamic.length !== 2) return null;
+ const primitive = (value: IrExpr): IrExpr =>
+ (["number", "string", "boolean", "bigint", "symbol"] as const)
+ .map((test): IrExpr => ({ kind: "dynTest", test, value, type: BOOL, loc }))
+ .reduce((a, b) => ({ kind: "logical", op: "||", left: a, right: b, type: BOOL, loc }));
+ const [left, right] = [args[0]!, args[1]!];
+ for (const i of operands)
+ args[i] = {
+ kind: "ternary",
+ cond: primitive(i === 0 ? right : left),
+ then: convert(args[i]!),
+ else_: args[i]!,
+ type: DYN,
+ loc,
+ };
+ } else for (const i of operands) args[i] = convert(args[i]!);
+ return {
+ kind: "seqExpr",
+ stmts: hoisted.flatMap((operand) => operand.stmts),
+ result: { ...expr, args },
+ type: expr.type,
+ loc,
+ };
+ }
+
+ /** The method and accessor names of boxed classes whose instances can
+ * carry their class's prototype data. */
+ private prototypeMemberNames(lowerer: Lowerer): Set {
+ const names = new Set();
+ for (const { info } of this.propertyReceivers(lowerer)) {
+ if (!hasClassPrototypeData(info)) continue;
+ for (let c: ClassInfo | null = info; c; c = c.base)
+ for (const name of c.methods.keys())
+ names.add(name.startsWith("get:") || name.startsWith("set:") ? name.slice(4) : name);
+ }
+ return names;
+ }
+
+ /** The methods some boxed class declares among valueOf and toString. */
+ private calledConversions(lowerer: Lowerer): Set<"valueOf" | "toString"> {
+ const methods = new Set<"valueOf" | "toString">();
+ for (const { info } of this.propertyReceivers(lowerer)) {
+ if (conversionMethod(lowerer, info, "valueOf")) methods.add("valueOf");
+ if (conversionMethod(lowerer, info, "toString")) methods.add("toString");
+ }
+ return methods;
+ }
+
+ /** `value.valueOf()` or `value.toString(argument)` on a dynamic receiver:
+ * a boxed instance of a class declaring the method calls it, any other
+ * value keeps the runtime method call. The toString result must be a
+ * string, as for the runtime's own-member call. */
+ private convertMethodCall(
+ lowerer: Lowerer,
+ fn: IrFunction,
+ expr: IrExpr,
+ calling: Set<"valueOf" | "toString">,
+ ): IrExpr | null {
+ if (expr.kind !== "libCall" || (expr.fn !== "dyn.valueOf" && expr.fn !== "dyn.toString"))
+ return null;
+ const method = expr.fn === "dyn.valueOf" ? "valueOf" : "toString";
+ const receiver = expr.args[0]!;
+ if (!calling.has(method)) return null;
+ if (receiver.kind === "varRef" && receiver.localId.startsWith(CONVERSION_CALL_PREFIX))
+ return null;
+ const loc = expr.loc;
+ const operands = this.hoistOperands(
+ fn,
+ method === "toString" ? [receiver, expr.args[1]!] : [receiver],
+ loc,
+ CONVERSION_CALL_PREFIX,
+ );
+ const values = operands.map((operand) => operand.value);
+ const helpers = this.conversionCallHelpers(lowerer, method, loc);
+ return {
+ kind: "seqExpr",
+ stmts: operands.flatMap((operand) => operand.stmts),
+ result: {
+ kind: "ternary",
+ cond: { kind: "call", callee: helpers.test.name, args: [values[0]!], type: BOOL, loc },
+ then: { kind: "call", callee: helpers.call.name, args: values, type: expr.type, loc },
+ else_: { ...expr, args: [...values, ...expr.args.slice(values.length)] },
+ type: expr.type,
+ loc,
+ },
+ type: expr.type,
+ loc,
+ };
+ }
+
+ private conversionCallHelpers(
+ lowerer: Lowerer,
+ method: "valueOf" | "toString",
+ loc: SrcLoc,
+ ): { test: IrFunction; call: IrFunction } {
+ let helpers = this.conversionCalls.get(method);
+ if (!helpers) {
+ const at = helperLoc(loc);
+ const params: IrParam[] = [
+ { localId: "p.0", name: "value", type: DYN },
+ ...(method === "toString" ? [{ localId: "p.1", name: "argument", type: DYN }] : []),
+ ];
+ const locals = params.map((param) => ({
+ id: param.localId,
+ name: param.name,
+ type: param.type,
+ mutable: false,
+ }));
+ const name = { kind: "strLit" as const, value: method, type: STRING, loc: at };
+ const test: IrFunction = {
+ name: `${TO_PRIMITIVE_PREFIX}${method}.test`,
+ params: params.slice(0, 1),
+ locals: locals.slice(0, 1),
+ returnType: BOOL,
+ body: [
+ {
+ kind: "return",
+ value: { kind: "boolLit", value: false, type: BOOL, loc: at },
+ loc: at,
+ },
+ ],
+ loc: at,
+ };
+ const call: IrFunction = {
+ name: `${TO_PRIMITIVE_PREFIX}${method}.call`,
+ params,
+ locals,
+ returnType: method === "toString" ? STRING : DYN,
+ body: [
+ {
+ kind: "return",
+ value:
+ method === "toString"
+ ? {
+ kind: "libCall",
+ fn: "dyn.toString",
+ args: [varRef("p.0", DYN, at), varRef("p.1", DYN, at), name],
+ type: STRING,
+ loc: at,
+ }
+ : {
+ kind: "libCall",
+ fn: "dyn.valueOf",
+ args: [varRef("p.0", DYN, at), name],
+ type: DYN,
+ loc: at,
+ },
+ loc: at,
+ },
+ ],
+ loc: at,
+ };
+ helpers = { test, call, classes: new Set() };
+ this.conversionCalls.set(method, helpers);
+ this.generated.add(test);
+ this.generated.add(call);
+ lowerer.liftedFns.push(test, call);
+ }
+ return helpers;
+ }
+
+ /** Evaluates each operand into a fresh local, in order. */
+ private hoistOperands(
+ fn: IrFunction,
+ operands: readonly IrExpr[],
+ loc: SrcLoc,
+ prefix = "%toPrimitive.operand.",
+ ): Hoisted[] {
+ return operands.map((operand) => {
+ const id = `${prefix}${fn.locals.length}`;
+ fn.locals.push({ id, name: "operand", type: operand.type, mutable: false });
+ const stmts: IrStmt[] = [{ kind: "varDecl", localId: id, init: operand, loc }];
+ return { stmts, value: varRef(id, operand.type, loc) };
+ });
+ }
+
+ /** The conversion helper for `hint`: a boxed class instance answers its
+ * OrdinaryToPrimitive result, any other value is returned unchanged for
+ * the runtime operation to convert. Arms are added per boxed class. */
+ private toPrimitiveHelper(lowerer: Lowerer, hint: ToPrimitiveHint, loc: SrcLoc): string {
+ let conversion = this.toPrimitives.get(hint);
+ if (!conversion) {
+ const at = helperLoc(loc);
+ const fn: IrFunction = {
+ name: `${TO_PRIMITIVE_PREFIX}${hint}`,
+ params: [{ localId: "p.0", name: "value", type: DYN }],
+ locals: [{ id: "p.0", name: "value", type: DYN, mutable: false }],
+ returnType: DYN,
+ body: [{ kind: "return", value: varRef("p.0", DYN, at), loc: at }],
+ loc: at,
+ };
+ conversion = { fn, classes: new Set() };
+ this.toPrimitives.set(hint, conversion);
+ this.generated.add(fn);
+ lowerer.liftedFns.push(fn);
+ }
+ return conversion.fn.name;
+ }
+
+ /** OrdinaryToPrimitive for one boxed class: the declared conversion
+ * methods in hint order, each result returned when it is a primitive.
+ * Object.prototype.valueOf answers the object itself and its toString
+ * answers "[object Object]". */
+ private toPrimitiveArm(
+ lowerer: Lowerer,
+ plan: PropertyReceiver,
+ hint: ToPrimitiveHint,
+ helper: IrFunction,
+ ): IrStmt | null {
+ const { info } = plan;
+ const loc = helper.loc;
+ const { condition } = this.propertyReceiver(plan, loc);
+ if (conversionMethod(lowerer, info, "sym:toPrimitive")) {
+ // A Symbol.toPrimitive method decides alone: its result must be a
+ // primitive.
+ const result = `%toPrimitive.exotic.${helper.locals.length}`;
+ helper.locals.push({ id: result, name: "exotic", type: DYN, mutable: false });
+ const hintValue: IrExpr = {
+ kind: "dynFrom",
+ value: { kind: "strLit", value: hint, type: STRING, loc },
+ type: DYN,
+ loc,
+ };
+ const body: IrStmt[] = [
+ {
+ kind: "varDecl",
+ localId: result,
+ init: {
+ kind: "call",
+ callee: this.toPrimitiveCall(lowerer, plan, "sym:toPrimitive", 1, loc),
+ args: [varRef("p.0", DYN, loc), hintValue],
+ type: DYN,
+ loc,
+ },
+ loc,
+ },
+ {
+ kind: "if",
+ cond: isPrimitive(varRef(result, DYN, loc), loc),
+ then: [{ kind: "return", value: varRef(result, DYN, loc), loc }],
+ else_: null,
+ loc,
+ },
+ {
+ kind: "return",
+ value: nodeThrowExpr(1, "", "Cannot convert object to primitive value", DYN, loc),
+ loc,
+ },
+ ];
+ return { kind: "if", cond: condition, then: body, else_: null, loc };
+ }
+ const order: ("valueOf" | "toString")[] =
+ hint === "string" ? ["toString", "valueOf"] : ["valueOf", "toString"];
+ if (!order.some((method) => conversionMethod(lowerer, info, method))) return null;
+ if (hint === "string" && !conversionMethod(lowerer, info, "toString")) return null;
+ const body: IrStmt[] = [];
+ let settled = false;
+ for (const method of order) {
+ if (!conversionMethod(lowerer, info, method)) {
+ if (method === "toString") {
+ body.push({
+ kind: "return",
+ value: {
+ kind: "dynFrom",
+ value: { kind: "strLit", value: "[object Object]", type: STRING, loc },
+ type: DYN,
+ loc,
+ },
+ loc,
+ });
+ settled = true;
+ break;
+ }
+ continue;
+ }
+ const result = `%toPrimitive.${method}.${helper.locals.length}`;
+ helper.locals.push({ id: result, name: method, type: DYN, mutable: false });
+ body.push(
+ {
+ kind: "varDecl",
+ localId: result,
+ init: {
+ kind: "call",
+ callee: this.toPrimitiveCall(lowerer, plan, method, 0, loc),
+ args: [varRef("p.0", DYN, loc)],
+ type: DYN,
+ loc,
+ },
+ loc,
+ },
+ {
+ kind: "if",
+ cond: isPrimitive(varRef(result, DYN, loc), loc),
+ then: [{ kind: "return", value: varRef(result, DYN, loc), loc }],
+ else_: null,
+ loc,
+ },
+ );
+ }
+ if (!settled)
+ body.push({
+ kind: "return",
+ value: nodeThrowExpr(1, "", "Cannot convert object to primitive value", DYN, loc),
+ loc,
+ });
+ return { kind: "if", cond: condition, then: body, else_: null, loc };
+ }
+
+ /** A function calling `method` with `arity` arguments on a boxed instance
+ * of `plan`'s class, as a dynamic method call would: an own replacement on
+ * the instance answers first for methods the program can replace. */
+ private toPrimitiveCall(
+ lowerer: Lowerer,
+ plan: PropertyReceiver,
+ method: ConversionMethod,
+ arity: number,
+ loc: SrcLoc,
+ ): string {
+ const key = JSON.stringify([plan.key, method, arity]);
+ const existing = this.toPrimitiveCalls.get(key);
+ if (existing) return existing.name;
+ const { info } = plan;
+ const { receiver } = this.propertyReceiver(plan, loc);
+ const args = Array.from({ length: arity }, (_, i) => varRef(`p.${i + 1}`, DYN, loc));
+ const source: Invoke = {
+ kind: "dynInvoke",
+ recv: varRef("p.0", DYN, loc),
+ method,
+ calleeName: method === "sym:toPrimitive" ? "value[Symbol.toPrimitive]" : `value.${method}`,
+ args,
+ type: DYN,
+ loc,
+ };
+ const value = varRef("p.0", DYN, loc);
+ let callback: IrExpr = dynUndefinedExpr(loc);
+ if (isClassCallback(lowerer, info, method)) {
+ const prototypeMethod = lowerer.prototypeMethodAccesses.has(method);
+ if (prototypeMethod) classPrototypeData(lowerer, info, loc);
+ const bag = this.readPropertyBagOrEmpty(lowerer, value, loc);
+ callback = {
+ kind: "ternary",
+ cond: prototypeMethod
+ ? { kind: "boolLit", value: true, type: BOOL, loc }
+ : {
+ kind: "libCall",
+ fn: "dyn.hasKey",
+ args: [bag, { kind: "strLit", value: method, type: STRING, loc }],
+ type: BOOL,
+ loc,
+ },
+ then: {
+ kind: "dynObjLit",
+ fields: [
+ {
+ key: { kind: "strLit", value: "value", type: STRING, loc },
+ value: {
+ kind: "dynKeyGet",
+ value: bag,
+ key: { kind: "strLit", value: method, type: STRING, loc },
+ type: DYN,
+ loc,
+ },
+ },
+ ],
+ type: DYN,
+ loc,
+ },
+ else_: dynUndefinedExpr(loc),
+ type: DYN,
+ loc,
+ };
+ }
+ const params: IrParam[] = [
+ { localId: "p.0", name: "value", type: DYN },
+ ...args.map((_, i) => ({ localId: `p.${i + 1}`, name: `p${i + 1}`, type: DYN })),
+ ];
+ const fn: IrFunction = {
+ name: `${TO_PRIMITIVE_PREFIX}call.${this.toPrimitiveCalls.size}`,
+ params,
+ locals: [
+ ...params.map((param) => ({
+ id: param.localId,
+ name: param.name,
+ type: param.type,
+ mutable: false,
+ })),
+ { id: "p.callback", name: "callback", type: DYN, mutable: false },
+ { id: "p.calleeName", name: "calleeName", type: STRING, mutable: false },
+ ],
+ returnType: DYN,
+ body: [],
+ loc,
+ };
+ const dispatch: Dispatch = { source, fn, callback: fn, classes: new Set() };
+ const before = lowerer.diags.length;
+ let body: IrStmt[];
+ try {
+ body = this.methodBody(lowerer, dispatch, info, receiver);
+ } catch (error) {
+ if (!(error instanceof PoisonError) || !info.decl) throw error;
+ const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" });
+ if (!fence) throw error;
+ body = [fence];
+ }
+ fn.body = [
+ { kind: "varDecl", localId: "p.callback", init: callback, loc },
+ {
+ kind: "varDecl",
+ localId: "p.calleeName",
+ init: { kind: "strLit", value: source.calleeName, type: STRING, loc },
+ loc,
+ },
+ ...body,
+ ];
+ this.toPrimitiveCalls.set(key, fn);
+ this.generated.add(fn);
+ lowerer.liftedFns.push(fn);
+ return fn.name;
+ }
+
private readPropertyBagOrEmpty(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr {
// One call per read site: large JS programs have a branch per class and
// property, so the absent-bag handling lives in the shared helper.
@@ -3159,10 +3894,20 @@ export class ClassDynamicDispatch {
obj: receiver,
value: transformExpr(initializers[0]!.value, {
stmt: (stmt) => stmt,
- expr: (expr) =>
- expr.kind === "dynCheck" && expr.value.kind === "varRef" && expr.value.localId === "p.0"
- ? receiver
- : expr,
+ expr: (expr) => {
+ const checked = (e: IrExpr): boolean =>
+ e.kind === "dynCheck" && e.value.kind === "varRef" && e.value.localId === "p.0";
+ // The checked receiver, possibly cast to this class (the prototype
+ // helper's argument), is the typed parameter itself.
+ if (
+ expr.kind === "downcast" &&
+ checked(expr.value) &&
+ expr.type.kind === "object" &&
+ expr.type.className === type.className
+ )
+ return receiver;
+ return checked(expr) ? receiver : expr;
+ },
}),
loc,
};
@@ -3191,6 +3936,79 @@ export class ClassDynamicDispatch {
return helper;
}
+ /** The prototype a new property bag of `current` (or of the subclass the
+ * receiver is) starts from, or null when `current` has no prototype data.
+ * A class with constructible subclasses that have prototype data selects
+ * it through its `%class.bagPrototype` helper; any other class reads its
+ * own prototype data. */
+ private bagPrototype(
+ lowerer: Lowerer,
+ current: ClassInfo,
+ receiver: IrExpr,
+ loc: SrcLoc,
+ ): IrExpr | null {
+ const arg: IrExpr =
+ receiver.type.kind === "object" && receiver.type.className !== current.def.name
+ ? {
+ kind: "downcast",
+ value: receiver,
+ type: { kind: "object", className: current.def.name },
+ loc,
+ }
+ : receiver;
+ const helper = this.bagPrototypeHelper(lowerer, current, loc);
+ return helper
+ ? { kind: "call", callee: helper.name, args: [arg], type: DYN, loc }
+ : classPrototypeData(lowerer, current, loc, arg);
+ }
+
+ /** `%class.bagPrototype:(this)`: the prototype data of the most
+ * derived constructible class `this` is an instance of, tested from the
+ * last subclass to the first and then the class itself. One helper per
+ * class, calling its subclasses' helpers, keeps every property-bag branch
+ * a single call: a whole hierarchy inlined into each branch grows with the
+ * square of its depth. Null when the class has no prototype data, in which
+ * case its subclasses are not tested either, or while no constructible
+ * subclass has any. The body is rebuilt on each use, as classes become
+ * constructible. */
+ private bagPrototypeHelper(lowerer: Lowerer, current: ClassInfo, loc: SrcLoc): IrFunction | null {
+ const type: IrType = { kind: "object", className: current.def.name };
+ const self = varRef("this", type, loc);
+ const own = classPrototypeData(lowerer, current, loc, self);
+ if (!own) return null;
+ const body: IrStmt[] = [];
+ const children = current.subclasses.filter((child) => lowerer.classCanBeConstructed(child));
+ for (const child of children.reverse()) {
+ const selected = this.bagPrototype(lowerer, child, self, loc);
+ if (!selected) continue;
+ body.push({
+ kind: "if",
+ cond: { kind: "instanceOf", value: self, className: child.def.name, type: BOOL, loc },
+ then: [{ kind: "return", value: selected, loc }],
+ else_: null,
+ loc,
+ });
+ }
+ let helper = this.bagPrototypeHelpers.get(current.def.name);
+ if (!helper && body.length === 0) return null;
+ body.push({ kind: "return", value: own, loc });
+ if (!helper) {
+ helper = {
+ name: `%class.bagPrototype:${current.def.name}`,
+ params: [{ localId: "this", name: "this", type }],
+ returnType: DYN,
+ locals: [{ id: "this", name: "this", type, mutable: false }],
+ body,
+ loc,
+ };
+ this.bagPrototypeHelpers.set(current.def.name, helper);
+ this.generated.add(helper);
+ lowerer.liftedFns.push(helper);
+ }
+ helper.body = body;
+ return helper;
+ }
+
/** Prototype state may appear in a later reachable body, so read-only bags
* settle only after the dispatch worklist closes. A read never stores: an
* absent empty bag stays undefined (callers test it), and an absent
@@ -3790,8 +4608,7 @@ export class ClassDynamicDispatch {
const body: IrStmt[] = [];
const symbol = symbolMemberKey(lowerer, info, name, loc);
if (symbol || (isClassOwnEnumerableFieldName(name) && !name.startsWith("sym:"))) {
- const prototypeMethod = lowerer.prototypeMethodAccesses.has(name);
- if (prototypeMethod) classPrototypeData(lowerer, info, loc, receiver);
+ const prototypeMethod = observedPrototypeMethod(lowerer, info, name, loc, receiver);
// Selecting the method must not give the object own storage.
const bag = this.readPropertyBag(lowerer, receiver, loc);
const key: IrExpr = symbol ?? { kind: "strLit", value: name, type: STRING, loc };
@@ -4210,6 +5027,8 @@ export class ClassDynamicDispatch {
let callee: string;
let virtual = false;
let owner: ClassInfo;
+ /** Erased instances of subclasses that redeclare a generic method. */
+ const overrides: { cls: ClassInfo; callee: string }[] = [];
if (methodInfo) {
owner = methodInfo.declarer;
params = methodInfo.sig.params;
@@ -4231,6 +5050,44 @@ export class ClassDynamicDispatch {
params = instance.params;
result = instance.returnType;
callee = instance.name;
+ } else if (
+ generic &&
+ !generic.info.implicitParams &&
+ generic.info.typeParams.length > 0 &&
+ generic.declarer.decl &&
+ !lowerer.overrideBelow(info, method)
+ ) {
+ // A generic method called by name runs its erased instance; a
+ // subclass that redeclares it runs its own, selected by the
+ // receiver's runtime class (most derived first).
+ owner = generic.declarer;
+ const instance = erasedGenericInstance(lowerer, generic.declarer.decl, generic.info);
+ params = instance.params;
+ result = instance.returnType;
+ callee = instance.name;
+ if (genericOverrideBelow(info, method)) {
+ const subclasses: ClassInfo[] = [];
+ const collect = (c: ClassInfo): void => {
+ for (const sub of c.subclasses) {
+ collect(sub);
+ if (sub.genericMethods?.has(method) && lowerer.classCanBeConstructed(sub))
+ subclasses.push(sub);
+ }
+ };
+ collect(info);
+ for (const sub of subclasses) {
+ const own = sub.genericMethods!.get(method)!;
+ if (!sub.decl || own.implicitParams || own.typeParams.length === 0) return fence();
+ const subInstance = erasedGenericInstance(lowerer, sub.decl, own);
+ if (
+ subInstance.params.length !== params.length ||
+ subInstance.params.some((p, i) => !typeEquals(p.type, params[i]!.type)) ||
+ !typeEquals(subInstance.returnType, result)
+ )
+ return fence();
+ overrides.push({ cls: sub, callee: subInstance.name });
+ }
+ }
} else return fence();
const incoming = dispatch.source.args.map((_, i) => varRef(`p.${i + 1}`, DYN, loc));
const args: IrExpr[] = [];
@@ -4262,7 +5119,7 @@ export class ClassDynamicDispatch {
if (!typeEquals(converted.type, param.type)) return fence();
args.push(converted);
}
- const call: IrExpr = virtual
+ let call: IrExpr = virtual
? {
kind: "virtualCall",
className: owner.def.name,
@@ -4278,6 +5135,29 @@ export class ClassDynamicDispatch {
type: result,
loc,
};
+ for (const override of overrides.toReversed()) {
+ const subType: IrType = { kind: "object", className: override.cls.def.name };
+ call = {
+ kind: "ternary",
+ cond: {
+ kind: "instanceOf",
+ value: receiver,
+ className: override.cls.def.name,
+ type: BOOL,
+ loc,
+ },
+ then: {
+ kind: "call",
+ callee: override.callee,
+ args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args],
+ type: result,
+ loc,
+ },
+ else_: call,
+ type: result,
+ loc,
+ };
+ }
let body: IrStmt[];
if (result.kind === "void")
body = [
diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
index b56d27262..8e7b1f152 100644
--- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts
+++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
@@ -48,9 +48,10 @@ export function collectClassMethodMutations(
let unknownOwnField = false;
// JavaScript method inference and dynamic class factories keep their
// existing dispatch contract. The native-call proof applies to typed
- // declarations, while writes in either source language can invalidate it.
+ // declarations (including JavaScript files TypeScript checks), while
+ // writes in either source language can invalidate it.
for (const file of files) {
- if (isJsSourceFile(file)) continue;
+ if (isJsSourceFile(file) && !lowerer.checkedJsFile(file)) continue;
ts.walkPreorder(file, (node) => {
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) typedClasses.push(node);
if (
@@ -212,7 +213,7 @@ export function collectClassMethodMutations(
receiver: ts.Expression,
site: ts.Node,
): boolean => {
- if (!ts.isDeleteExpression(site) || !ts.isIdentifier(key)) return false;
+ if (!ts.isDeleteExpression(site) || !ts.isIdentifier(unwrap(key))) return false;
const statement = site.parent;
if (!ts.isExpressionStatement(statement)) return false;
const loop = statement.parent;
@@ -230,9 +231,11 @@ export function collectClassMethodMutations(
);
};
const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => {
- key = unwrap(key);
- if (ts.isStringLiteralLike(key)) markName(key.text, site);
- else if (ts.isNumericLiteral(key)) markName(String(Number(key.text)), site);
+ const value = unwrap(key);
+ if (ts.isStringLiteralLike(value)) markName(value.text, site);
+ else if (ts.isNumericLiteral(value)) markName(String(Number(value.text)), site);
+ // The key keeps its assertions: `slots[at as number]` and `slots[at!]`
+ // write a number key whatever the type of `at`.
else computedWrites.push({ key, receiver, site });
};
const markFields = (value: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => {
@@ -547,7 +550,7 @@ export function collectClassMethodMutations(
}
for (const { key, receiver, site } of computedWrites) {
const type = key ? lowerer.typeOf(key) : undefined;
- const names = type && (literalValues(type) ?? (key ? literalLoopKeys(key) : null));
+ const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null));
if (names) {
for (const name of names) markName(String(name), site);
continue;
diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts
index 4267a186a..7c8770021 100644
--- a/packages/compiler/src/frontend/lowering/class-method-values.ts
+++ b/packages/compiler/src/frontend/lowering/class-method-values.ts
@@ -24,7 +24,7 @@ import {
type ClassInfo,
} from "./lower-classes.js";
import { funcTypeFromParamShapes, type ParamShape } from "./call-signatures.js";
-import { implicitDefaultInstance } from "./generic-functions.js";
+import { erasedGenericInstance, implicitDefaultInstance } from "./generic-functions.js";
import { errorToStringMethod } from "./error-methods.js";
import { classCallbackValue, isClassCallback } from "./class-callbacks.js";
@@ -225,6 +225,20 @@ export function classMethodValue(
params = instance.params;
ret = instance.returnType;
callee = instance.name;
+ } else if (
+ generic &&
+ !generic.info.implicitParams &&
+ generic.info.typeParams.length > 0 &&
+ generic.declarer.decl &&
+ !lowerer.overrideBelow(info, method) &&
+ !genericOverrideBelow(info, method)
+ ) {
+ // A generic method as a value: its erased instance.
+ owner = generic.declarer;
+ const instance = erasedGenericInstance(lowerer, blame, generic.info);
+ params = instance.params;
+ ret = instance.returnType;
+ callee = instance.name;
} else {
lowerer.unsupported("SC1090", blame, "values of builtin or unspecialized generic methods");
}
diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts
index a1486a617..3c610c51a 100644
--- a/packages/compiler/src/frontend/lowering/class-protocols.ts
+++ b/packages/compiler/src/frontend/lowering/class-protocols.ts
@@ -1,5 +1,4 @@
import * as ts from "../ts7/adapter.js";
-import { isGenericCallableMemberType } from "../type-mapper.js";
/** Index only program classes. Ambient declarations describe APIs, not
* compiled implementations, and must not change a protocol's storage. */
@@ -708,15 +707,6 @@ export class ClassProtocols {
const protocols = this.index();
const symbol = type.getSymbol();
const properties = this.checker.getPropertiesOfType(type);
- // Generic-only members have no single runtime callable signature. Keep
- // their existing declaration-based specialization and refusal boundary.
- if (
- properties.length > 0 &&
- properties.every((property) =>
- isGenericCallableMemberType(this.checker.getTypeOfSymbol(property), this.checker),
- )
- )
- return false;
if (symbol && protocols.has(symbol)) return true;
if (
!symbol ||
@@ -731,7 +721,6 @@ export class ClassProtocols {
return false;
for (const property of properties) {
const memberType = this.checker.getTypeOfSymbol(property);
- if (isGenericCallableMemberType(memberType, this.checker)) continue;
if (this.checker.getCallSignatures(memberType).length === 0) continue;
for (const declaration of this.methods.get(property.name) ?? []) {
const classSymbol = declaration.name
diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts
index 77f7cb541..1db709e91 100644
--- a/packages/compiler/src/frontend/lowering/class-prototypes.ts
+++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts
@@ -229,6 +229,27 @@ export function classPrototypeData(
return { kind: "call", callee: info.def.prototypeDataHelper, args: [], type: DYN, loc };
}
+/** Whether reads of method `name` on instances of `info` use the observed
+ * prototype slot: the census marked the name and the class has prototype
+ * data that holds the method. Classes without prototype data (generic
+ * instantiations, mixins and their subclasses) and function-local classes
+ * check for the property and otherwise use the declared method. */
+export function observedPrototypeMethod(
+ lowerer: Lowerer,
+ info: ClassInfo,
+ name: string,
+ loc: SrcLoc,
+ receiver?: IrExpr,
+): boolean {
+ if (!lowerer.prototypeMethodAccesses.has(name)) return false;
+ // Building the data also marks a function-local class whose instances
+ // need it. That class value's prototype holds only methods the program
+ // replaced there, so its reads check for the key (own or inherited) and
+ // otherwise use the declared method.
+ const data = classPrototypeData(lowerer, info, loc, receiver);
+ return data !== null && info.localClass === undefined;
+}
+
export function hasClassPrototypeData(info: ClassInfo): boolean {
return (
info.callableBase !== undefined ||
diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts
index ca7495dd7..0356b714e 100644
--- a/packages/compiler/src/frontend/lowering/coercions/unions.ts
+++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts
@@ -77,6 +77,48 @@ export function narrowedRetagHelper(
return lowerer.unionRetagHelper(fromId, toId, loc, trappable);
}
+/** A union conversion whose source arms include an object type with a
+ * required property of type `never` (the error arm of `Result`).
+ * No value has that type, but its arm still has a storage layout (never
+ * maps to the f64 placeholder) that no destination arm matches. When the
+ * node's checker type proves a source arm uninhabited, the re-tag traps on
+ * it instead of needing a destination. Optional `never` properties,
+ * `never[]` and functions returning `never` have values and keep their
+ * arms. Null when no source arm is proven uninhabited. */
+export function uninhabitedArmRetagHelper(
+ lowerer: Lowerer,
+ node: ts.Node,
+ fromId: string,
+ toId: string,
+ loc: SrcLoc,
+): string | null {
+ const type = lowerer.typeOf(node);
+ if (!type.isUnionType()) return null;
+ const uninhabited = new Set();
+ const inhabited = new Set();
+ for (const part of ts.constituentTypes(type)) {
+ const arm = lowerer.mapTypeOf(part);
+ const tag = arm ? lowerer.armTag(fromId, arm) : -1;
+ if (tag < 0) continue;
+ (hasRequiredNeverProperty(lowerer, part) ? uninhabited : inhabited).add(tag);
+ }
+ // An arm shared with an inhabited constituent holds real values.
+ for (const tag of inhabited) uninhabited.delete(tag);
+ if (uninhabited.size === 0) return null;
+ return lowerer.unionRetagHelper(fromId, toId, loc, uninhabited);
+}
+
+function hasRequiredNeverProperty(lowerer: Lowerer, type: ts.Type): boolean {
+ if ((type.flags & ts.TypeFlags.Object) === 0) return false;
+ return lowerer.checker
+ .getPropertiesOfType(type)
+ .some(
+ (property) =>
+ (property.flags & ts.SymbolFlags.Optional) === 0 &&
+ (lowerer.checker.getTypeOfSymbol(property).flags & ts.TypeFlags.Never) !== 0,
+ );
+}
+
/** The stranded-UNIT trap for PLAIN (non-union) slots: a null/undefined
* value flowing into a non-nullable typed slot the checker approved —
* `null!` and `null as any as T` casts, and the non-strict world's
diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts
index b1e6aa98f..a79864983 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts
@@ -709,6 +709,30 @@ function arrayUnionHofHelper(
return name;
}
+/** A kept filter element as the result's element type: the value itself,
+ * or its narrowed arm or class view when the predicate narrowed it. */
+function filterRetainedValue(
+ lowerer: Lowerer,
+ v: IrExpr,
+ elem: IrType,
+ outElem: IrType,
+ loc: SrcLoc,
+): IrExpr {
+ if (typeEquals(outElem, elem)) return v;
+ const classView =
+ outElem.kind === "object" ? checkedClassAssertion(lowerer, v, outElem, loc) : null;
+ if (classView !== null) return classView;
+ if (v.type.kind !== "union") return v;
+ return {
+ kind: "unionNarrow",
+ unionId: v.type.unionId,
+ tag: lowerer.armTag(v.type.unionId, outElem),
+ value: v,
+ type: outElem,
+ loc,
+ };
+}
+
/** The loop body of one synthetic array HOF, from existing IR nodes:
*
* map: out = []; n = a.length; for (i = 0; i < n; i++) out.push(f(a[i])); return out;
@@ -914,24 +938,7 @@ function buildArrayHofFn(
{ id: "v.0", name: "v", type: valueT, mutable: false },
);
returnType = outT;
- const classView =
- !typeEquals(outElem, elem) && outElem.kind === "object"
- ? checkedClassAssertion(lowerer, varRef("v.0", valueT, loc), outElem, loc)
- : null;
- const retained = typeEquals(outElem, elem)
- ? varRef("v.0", valueT, loc)
- : classView !== null
- ? classView
- : valueT.kind === "union"
- ? {
- kind: "unionNarrow" as const,
- unionId: valueT.unionId,
- tag: lowerer.armTag(valueT.unionId, outElem),
- value: varRef("v.0", valueT, loc),
- type: outElem,
- loc,
- }
- : varRef("v.0", valueT, loc);
+ const retained = filterRetainedValue(lowerer, varRef("v.0", valueT, loc), elem, outElem, loc);
body = [
{
kind: "varDecl",
@@ -1039,16 +1046,7 @@ export function lowerArrayFindLikeCall(
// `packages.some((p) => p.scripts[name])` idiom, whose callback
// returns the index read's `string | undefined`).
const fnRet = fnArg.type.ret;
- const truthyRet =
- fnRet.kind === "bool" ||
- fnRet.kind === "f64" ||
- fnRet.kind === "string" ||
- fnRet.kind === "dyn" ||
- (fnRet.kind === "union" &&
- (lowerer.unions.get(fnRet.unionId)?.arms ?? []).every(
- (a) => a.kind !== "dyn" && a.kind !== "caught",
- ));
- if (!truthyRet) lowerer.badType(argNode, lowerer.typeOf(argNode));
+ if (!findPredicateOk(lowerer, fnRet)) lowerer.badType(argNode, lowerer.typeOf(argNode));
const last = method === "findLast" || method === "findLastIndex";
if (
method === "some" ||
@@ -1116,42 +1114,7 @@ export function lowerArrayFindLikeCall(
if (resultT.kind !== "union") lowerer.badType(call, lowerer.typeOf(call)); // defensive: T | undefined always maps to a union
const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
if (undefTag < 0) lowerer.badType(call, lowerer.typeOf(call));
- let retag: string | null = null;
- if (!lowerer.coercibleValue(valueT, resultT)) {
- const inline = ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode);
- if (!unchangedElement && (!inline || argNode.type !== undefined)) {
- lowerer.unsupported(
- "SC1090",
- argNode,
- `narrowing '.${method}' requires an inline callback with an inferred predicate (annotate the return ': boolean' to keep the receiver's element type)`,
- );
- }
- const signature = inline ? lowerer.checker.getSignatureFromDeclaration(argNode) : undefined;
- const predicate = signature
- ? lowerer.checker.getTypePredicateOfSignature(signature)
- : undefined;
- if (
- (!unchangedElement && (!predicate || predicate.parameterIndex !== 0)) ||
- valueT.kind !== "union"
- ) {
- lowerer.unsupported(
- "SC1090",
- call,
- `'.${method}' result narrowing without a predicate over its element`,
- );
- }
- retag = lowerer.narrowedRetagHelper(call, valueT.unionId, resultT.unionId, loc);
- const present = lowerer.stripUndefinedArm(resultT);
- if (
- retag === null &&
- !(present.kind === "object" && canAssertClassValue(lowerer, valueT, present))
- )
- lowerer.unsupported(
- "SC1090",
- call,
- `'.${method}' narrowing to an incompatible result layout`,
- );
- }
+ const retag = findResultRetag(lowerer, call, argNode, method, valueT, resultT, unchangedElement);
const helper = findHelper(
lowerer,
elem,
@@ -1168,6 +1131,67 @@ export function lowerArrayFindLikeCall(
return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
}
+/** The retag helper that converts a found element into find's result union,
+ * or null when the element converts directly. Only an inline callback with
+ * an inferred predicate over its element can narrow the result; a written
+ * predicate is an unchecked assertion and keeps the refusal. */
+function findResultRetag(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ argNode: ts.Expression,
+ method: string,
+ valueT: IrType,
+ resultT: IrType & { kind: "union" },
+ unchangedElement: boolean,
+): string | null {
+ if (lowerer.coercibleValue(valueT, resultT)) return null;
+ const loc = locOf(call);
+ const inline = ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode);
+ if (!unchangedElement && (!inline || argNode.type !== undefined)) {
+ lowerer.unsupported(
+ "SC1090",
+ argNode,
+ `narrowing '.${method}' requires an inline callback with an inferred predicate (annotate the return ': boolean' to keep the receiver's element type)`,
+ );
+ }
+ const signature = inline ? lowerer.checker.getSignatureFromDeclaration(argNode) : undefined;
+ const predicate = signature ? lowerer.checker.getTypePredicateOfSignature(signature) : undefined;
+ if (
+ (!unchangedElement && (!predicate || predicate.parameterIndex !== 0)) ||
+ valueT.kind !== "union"
+ ) {
+ lowerer.unsupported(
+ "SC1090",
+ call,
+ `'.${method}' result narrowing without a predicate over its element`,
+ );
+ }
+ const retag = lowerer.narrowedRetagHelper(call, valueT.unionId, resultT.unionId, loc);
+ const present = lowerer.stripUndefinedArm(resultT);
+ if (
+ retag === null &&
+ !(present.kind === "object" && canAssertClassValue(lowerer, valueT, present))
+ )
+ lowerer.unsupported("SC1090", call, `'.${method}' narrowing to an incompatible result layout`);
+ return retag;
+}
+
+/** The find-like predicate results whose ToBoolean has a static answer:
+ * bool passes through; f64/string by value; checked values and
+ * truthy-answerable unions by their runtime kind or arm. */
+function findPredicateOk(lowerer: Lowerer, fnRet: IrType): boolean {
+ return (
+ fnRet.kind === "bool" ||
+ fnRet.kind === "f64" ||
+ fnRet.kind === "string" ||
+ fnRet.kind === "dyn" ||
+ (fnRet.kind === "union" &&
+ (lowerer.unions.get(fnRet.unionId)?.arms ?? []).every(
+ (a) => a.kind !== "dyn" && a.kind !== "caught",
+ ))
+ );
+}
+
/** ToBoolean of a predicate result inside a synthesized HOF helper — the
* ensureBool subset the find-like path admits (bool through; checked
* values via dynTest; f64/string/truthy-answerable unions via toBool). */
@@ -1208,17 +1232,7 @@ function findHelper(
const fnT = funcOf([valueT, F64, arrT].slice(0, arity), fnRet);
const v = varRef("v.0", valueT, loc);
- const presentArm = lowerer.stripUndefinedArm(resultT);
- // A successful inferred class predicate may select a descendant of a
- // stored base arm. Keep the value read before the callback mutated the array.
- const classView =
- retag === null && !lowerer.coercibleValue(valueT, resultT) && presentArm.kind === "object"
- ? checkedClassAssertion(lowerer, v, presentArm, loc)
- : null;
- const found: IrExpr =
- retag === null
- ? lowerer.coerceToExpected(classView ?? v, resultT)
- : { kind: "call", callee: retag, args: [v], type: resultT, loc };
+ const found = findFoundValue(lowerer, v, resultT, retag, loc);
const miss: IrExpr = {
kind: "unionWrap",
unionId: resultT.unionId,
@@ -1272,6 +1286,25 @@ function findHelper(
return name;
}
+/** A found element as find's result union. A successful inferred class
+ * predicate may select a descendant of a stored base arm. The value is the
+ * one read before the callback ran, even if the callback mutated its slot. */
+function findFoundValue(
+ lowerer: Lowerer,
+ v: IrExpr,
+ resultT: IrType & { kind: "union" },
+ retag: string | null,
+ loc: SrcLoc,
+): IrExpr {
+ if (retag !== null) return { kind: "call", callee: retag, args: [v], type: resultT, loc };
+ const presentArm = lowerer.stripUndefinedArm(resultT);
+ const classView =
+ !lowerer.coercibleValue(v.type, resultT) && presentArm.kind === "object"
+ ? checkedClassAssertion(lowerer, v, presentArm, loc)
+ : null;
+ return lowerer.coerceToExpected(classView ?? v, resultT);
+}
+
/** The findIndex/findLastIndex loop — find's index-returning sibling, no
* union; findLastIndex is the identical loop walked backwards:
*
@@ -2580,3 +2613,200 @@ function buildDynFilterFn(name: string, elemT: IrType, arity: number, loc: SrcLo
loc,
};
}
+
+/** Tuple `filter` / `find` / `findIndex` / `some` / `every`. `reads` holds
+ * one element read per position, in index order, each over the helper's
+ * `a.0` receiver parameter. The helper visits positions in order and reads
+ * each one only when it is visited, so a callback's write to a later
+ * position is visible; filter and find keep the value read before the
+ * callback ran. find, findIndex, some and every return at the first
+ * deciding position. Predicate results and narrowing follow the array
+ * methods' rules. */
+export function lowerTuplePredicateCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ method: "filter" | "find" | "findIndex" | "some" | "every",
+ receiver: IrExpr,
+ elem: IrType,
+ reads: IrExpr[],
+): IrExpr {
+ const loc = locOf(call);
+ const argNode = call.arguments[0]!;
+ const booleanFilter =
+ method === "filter" &&
+ ts.isIdentifier(argNode) &&
+ argNode.text === "Boolean" &&
+ lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined);
+ if (booleanFilter) {
+ if (elem.kind === "union") lowerer.requireTruthyUnion(elem.unionId, argNode);
+ if (elem.kind === "dyn" || elem.kind === "void" || isUnitType(elem)) {
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
+ }
+ }
+ const { fnArg, arity } = booleanFilter
+ ? { fnArg: truthyFilterCallback(lowerer, elem, loc), arity: 1 }
+ : lowerArrayCallback(lowerer, argNode, [elem], receiver.type);
+ const fnRet = fnArg.type.ret;
+ if (method === "filter" ? !filterPredicateOk(lowerer, fnRet) : !findPredicateOk(lowerer, fnRet)) {
+ if (fnRet.kind === "void") {
+ lowerer.unsupported(
+ "SC1090",
+ argNode,
+ `'.${method}()' with a void-returning predicate (the callback return value is erased before its truthiness can be tested)`,
+ );
+ }
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
+ }
+ let resultT: IrType;
+ let outElem: IrType = elem;
+ // A narrowing find retags from the element union widened with undefined,
+ // the value type an array find narrows from.
+ let foundT: IrType = elem;
+ let retag: string | null = null;
+ if (method === "filter") {
+ // The element union is built in position order, so the checker's
+ // result element can list the same arms in another order.
+ const checkerResult = lowerer.mapTypeOf(lowerer.typeOf(call));
+ const arms = (type: IrType): IrType[] =>
+ type.kind === "union" ? lowerer.unions.get(type.unionId)!.arms : [type];
+ const unchanged =
+ checkerResult?.kind === "array" &&
+ arms(checkerResult.elem).length === arms(elem).length &&
+ arms(checkerResult.elem).every((arm) => arms(elem).some((other) => typeEquals(arm, other)));
+ outElem = unchanged
+ ? elem
+ : filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter);
+ resultT = arrayOf(outElem);
+ } else if (method === "find") {
+ resultT = lowerer.irTypeOf(call);
+ if (resultT.kind !== "union" || lowerer.armTag(resultT.unionId, UNDEFINED_T) < 0) {
+ lowerer.badType(call, lowerer.typeOf(call));
+ }
+ if (!lowerer.coercibleValue(elem, resultT)) foundT = arrayValueType(lowerer, elem);
+ retag = findResultRetag(lowerer, call, argNode, method, foundT, resultT, false);
+ } else {
+ resultT = method === "findIndex" ? F64 : BOOL;
+ }
+ const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`;
+ let helper = lowerer.arrHofHelpers.get(key);
+ if (!helper) {
+ helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`;
+ lowerer.arrHofHelpers.set(key, helper);
+ const v = varRef("v.0", elem, loc);
+ const out = varRef("out.0", resultT, loc);
+ const test = (index: number): IrExpr => {
+ const invocation: IrExpr = {
+ kind: "callValue",
+ callee: varRef("f.0", fnArg.type, loc),
+ args: [v, numLit(index, loc)].slice(0, arity),
+ type: fnRet,
+ loc,
+ };
+ return method === "filter"
+ ? filterCond(invocation, fnRet, loc)
+ : predicateToBoolean(invocation);
+ };
+ const decided = (index: number): IrStmt => {
+ switch (method) {
+ case "filter":
+ return arrayValueStore(
+ lowerer,
+ out,
+ { kind: "arrIntrinsic", method: "length", receiver: out, args: [], type: F64, loc },
+ filterRetainedValue(lowerer, v, elem, outElem, loc),
+ outElem,
+ loc,
+ );
+ case "find":
+ return {
+ kind: "return",
+ value: findFoundValue(
+ lowerer,
+ lowerer.coerceToExpected(v, foundT),
+ resultT as IrType & { kind: "union" },
+ retag,
+ loc,
+ ),
+ loc,
+ };
+ case "findIndex":
+ return { kind: "return", value: numLit(index, loc), loc };
+ case "some":
+ return { kind: "return", value: boolLit(true, loc), loc };
+ case "every":
+ return { kind: "return", value: boolLit(false, loc), loc };
+ }
+ };
+ const body: IrStmt[] =
+ method === "filter"
+ ? [
+ {
+ kind: "varDecl",
+ localId: "out.0",
+ init: { kind: "arrayLit", elems: [], type: resultT, loc },
+ loc,
+ },
+ ]
+ : [];
+ reads.forEach((read, index) => {
+ body.push(
+ index === 0
+ ? { kind: "varDecl", localId: "v.0", init: read, loc }
+ : { kind: "assign", localId: "v.0", value: read, loc },
+ {
+ kind: "if",
+ cond:
+ method === "every"
+ ? { kind: "unary", op: "!", operand: test(index), type: BOOL, loc }
+ : test(index),
+ then: [decided(index)],
+ else_: null,
+ loc,
+ },
+ );
+ });
+ let done: IrExpr;
+ switch (method) {
+ case "filter":
+ done = out;
+ break;
+ case "find": {
+ const unionId = (resultT as IrType & { kind: "union" }).unionId;
+ done = {
+ kind: "unionWrap",
+ unionId,
+ tag: lowerer.armTag(unionId, UNDEFINED_T),
+ value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
+ type: resultT,
+ loc,
+ };
+ break;
+ }
+ case "findIndex":
+ done = numLit(-1, loc);
+ break;
+ default:
+ done = boolLit(method === "every", loc);
+ }
+ body.push({ kind: "return", value: done, loc });
+ lowerer.liftedFns.push({
+ name: helper,
+ params: [
+ { localId: "a.0", name: "a", type: receiver.type },
+ { localId: "f.0", name: "f", type: fnArg.type },
+ ],
+ returnType: resultT,
+ locals: [
+ { id: "a.0", name: "a", type: receiver.type, mutable: false },
+ { id: "f.0", name: "f", type: fnArg.type, mutable: false },
+ ...(reads.length > 0 ? [{ id: "v.0", name: "v", type: elem, mutable: true }] : []),
+ ...(method === "filter"
+ ? [{ id: "out.0", name: "out", type: resultT, mutable: false }]
+ : []),
+ ],
+ body,
+ loc,
+ });
+ }
+ return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
index 88c542ec0..015cb2123 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
@@ -1004,6 +1004,10 @@ export function lowerArrayFromCall(
if (src.type.kind === "dyn" && args.length === 1) {
return lowerCheckedArrayFrom(lowerer, src, loc);
}
+ // Array.from(sp): the checked-dynamic view's live pair iteration.
+ if (src.type.kind === "searchParams" && args.length === 1) {
+ return lowerCheckedArrayFrom(lowerer, lowerer.coerceInto(args[0]!, src, DYN), loc);
+ }
// A generator (or an iterator adapted into one) drains in order.
if (src.type.kind === "generator" && args.length === 1) {
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
diff --git a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts
index 43e8b4959..db60cdf1b 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts
@@ -17,9 +17,10 @@ import { locOf } from "../../program.js";
import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js";
import { newFnCtx } from "../function-context.js";
import { lowerArrayCallback } from "./callback-arguments.js";
+import { extractIteratorValue, resultRecordOf, type GenType } from "../lower-generators.js";
-/** `Array.fromAsync(source, mapFn?)` over an array, as an async helper
- * with the spec's loop:
+/** `Array.fromAsync(source, mapFn?)` over an array or an async generator,
+ * as an async helper with the spec's loop:
*
* const out = []; let k = 0;
* for await (const v of source) out.push(mapFn ? await mapFn(v, k++) : v);
@@ -29,9 +30,10 @@ import { lowerArrayCallback } from "./callback-arguments.js";
* awaited (a promise element through its promise, any other value as one
* resolved promise), and the iterator result is awaited again, which the
* helper spells as the same microtask hops the `for await` array loop
- * takes. A throwing or rejecting mapFn, and a rejected element, close the
- * iterator before the returned promise rejects. Null when this is not an
- * `Array.fromAsync` call. */
+ * takes. An async generator is driven directly: each next() result is
+ * awaited once and its value is used as is. A throwing or rejecting mapFn,
+ * and a rejected array element, close the iterator before the returned
+ * promise rejects. Null when this is not an `Array.fromAsync` call. */
export function lowerArrayFromAsyncCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -54,23 +56,18 @@ export function lowerArrayFromAsyncCall(
lowerer.badType(call, lowerer.typeOf(call));
const outT = resultT.inner;
const source = lowerer.lowerExpr(args[0]!);
- if (source.type.kind !== "array") {
- if (source.type.kind === "generator" && source.type.async) {
- // An async generator's completion settles one microtask later here
- // than in Node, which would reorder the returned promise.
- lowerer.noLowering(
- "Array.fromAsync over an async generator",
- args[0]!,
- "collect its values with for await",
- );
- }
+ let step: SourceStep;
+ if (source.type.kind === "array") {
+ step = arraySourceStep(lowerer, args[0]!, source.type.elem);
+ } else if (source.type.kind === "generator" && source.type.async) {
+ step = generatorSourceStep(lowerer, args[0]!, source.type);
+ } else {
lowerer.noLowering(
`Array.fromAsync over a value of type '${lowerer.checker.typeToString(lowerer.typeOf(args[0]!))}'`,
args[0]!,
- "an array is the supported source",
+ "an array or an async generator is the supported source",
);
}
- const step = arraySourceStep(lowerer, args[0]!, source.type.elem);
let mapper: (IrExpr & { type: IrType & { kind: "func" } }) | null = null;
if (args[1]) {
const callback = lowerArrayCallback(lowerer, args[1], [step.valueT], outT);
@@ -207,19 +204,173 @@ function nonThenable(lowerer: Lowerer, type: IrType): boolean {
);
}
+interface SourceStep {
+ /** The value each step hands to the mapping function. */
+ valueT: IrType;
+ loop(source: IrExpr, append: (value: IrExpr) => IrStmt[], loc: SrcLoc): IrStmt[];
+}
+
+/** The async generator loop: each next() result is awaited, `done` ends
+ * the loop, and the value goes to the mapping function as is (no further
+ * await, unlike the async-from-sync array form). A mapping function that
+ * throws or rejects closes the generator by awaiting return(); a failure
+ * of that close is dropped, so the mapping error rejects the result. A
+ * rejected next() does not close the generator and rejects the result on
+ * the turn Node rejects it. */
+function generatorSourceStep(lowerer: Lowerer, node: ts.Expression, genT: GenType): SourceStep {
+ if (genT.yieldT.kind === "void") {
+ lowerer.noLowering("Array.fromAsync over an async generator that never yields", node);
+ }
+ if (genT.nextT.kind !== "undefinedT" && genT.nextT.kind !== "dyn") {
+ lowerer.noLowering(
+ `Array.fromAsync over an async generator whose yields expect .next(value) ('${lowerer.fmt(genT.nextT)}')`,
+ node,
+ );
+ }
+ const recT = resultRecordOf(lowerer, genT);
+ const promiseT: IrType = { kind: "promise", inner: recT };
+ const fieldT = lowerer.shapes.get(recT.shapeId)!.fields.find((f) => f.name === "value")!.type;
+ return {
+ valueT: genT.yieldT,
+ loop(source, append, loc) {
+ const g = lowerer.declareHiddenLocal("%fromAsyncGenerator", genT);
+ const needsClose = lowerer.declareHiddenLocal("%fromAsyncNeedsClose", BOOL);
+ needsClose.mutable = true;
+ const p = lowerer.declareHiddenLocal("%fromAsyncNext", promiseT);
+ const r = lowerer.declareHiddenLocal("%fromAsyncResult", recT);
+ r.mutable = true;
+ const nextError = lowerer.declareHiddenLocal("%fromAsyncNextError", CAUGHT);
+ const closeError = lowerer.declareHiddenLocal("%fromAsyncCloseError", CAUGHT);
+ const gRef = varRef(g.id, genT, loc);
+ const rRef = varRef(r.id, recT, loc);
+ const read: IrExpr = {
+ kind: "recordGet",
+ obj: rRef,
+ shapeId: recT.shapeId,
+ field: "value",
+ type: fieldT,
+ loc,
+ };
+ const value = extractIteratorValue(lowerer, genT.yieldT, fieldT, read, loc);
+ if (!value) {
+ lowerer.noLowering(
+ `Array.fromAsync over an async generator yielding '${lowerer.fmt(genT.yieldT)}'`,
+ node,
+ );
+ }
+ const setClose = (on: boolean): IrStmt => ({
+ kind: "assign",
+ localId: needsClose.id,
+ value: boolLit(on, loc),
+ loc,
+ });
+ const loop: IrStmt = {
+ kind: "while",
+ cond: boolLit(true, loc),
+ body: [
+ {
+ kind: "varDecl",
+ localId: p.id,
+ init: { kind: "genResume", mode: "next", gen: gRef, arg: null, type: promiseT, loc },
+ loc,
+ },
+ { kind: "varDecl", localId: r.id, init: null, loc },
+ // A rejected next() rejects the result two reactions later, as
+ // the rejected array element does.
+ {
+ kind: "tryCatch",
+ tryBody: [
+ {
+ kind: "assign",
+ localId: r.id,
+ value: { kind: "awaitExpr", value: varRef(p.id, promiseT, loc), type: recT, loc },
+ loc,
+ },
+ ],
+ catchBody: [hop(loc), hop(loc), { kind: "rethrow", localId: nextError.id, loc }],
+ catchLocalId: nextError.id,
+ finallyBody: null,
+ loc,
+ },
+ {
+ kind: "if",
+ cond: {
+ kind: "recordGet",
+ obj: rRef,
+ shapeId: recT.shapeId,
+ field: "done",
+ type: BOOL,
+ loc,
+ },
+ then: [{ kind: "break", loc }],
+ else_: null,
+ loc,
+ },
+ setClose(true),
+ ...append(value!),
+ setClose(false),
+ ],
+ loc,
+ };
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: g.id, init: source, loc },
+ { kind: "varDecl", localId: needsClose.id, init: boolLit(false, loc), loc },
+ {
+ kind: "tryCatch",
+ tryBody: [loop],
+ catchBody: null,
+ catchLocalId: null,
+ finallyBody: [
+ {
+ kind: "if",
+ cond: varRef(needsClose.id, BOOL, loc),
+ then: [closeGenerator(gRef, promiseT, recT, closeError.id, loc)],
+ else_: null,
+ loc,
+ },
+ ],
+ loc,
+ },
+ ];
+ return stmts;
+ },
+ };
+}
+
+/** Await `gen.return()`, dropping a failure of the close. */
+function closeGenerator(
+ gen: IrExpr,
+ promiseT: IrType,
+ recT: IrType,
+ errorLocalId: string,
+ loc: SrcLoc,
+): IrStmt {
+ return {
+ kind: "tryCatch",
+ tryBody: [
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "awaitExpr",
+ value: { kind: "genResume", mode: "return", gen, arg: null, type: promiseT, loc },
+ type: recT,
+ loc,
+ },
+ loc,
+ },
+ ],
+ catchBody: [],
+ catchLocalId: errorLocalId,
+ finallyBody: null,
+ loc,
+ };
+}
+
/** The `for await` array loop (lowerForOf's async-from-sync form): two
* hops per element that is not a promise, the element's own await plus
* one hop for a promise element, two hops before the final done result,
* and one hop to close the iterator when the body completes abruptly. */
-function arraySourceStep(
- lowerer: Lowerer,
- node: ts.Expression,
- elem: IrType,
-): {
- /** The awaited value each element hands to the mapping function. */
- valueT: IrType;
- loop(source: IrExpr, append: (value: IrExpr) => IrStmt[], loc: SrcLoc): IrStmt[];
-} {
+function arraySourceStep(lowerer: Lowerer, node: ts.Expression, elem: IrType): SourceStep {
const yieldedT = arrayValueType(lowerer, elem);
let promiseTag: number | null = null;
let valueT = yieldedT;
diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
index ea821f006..40c2e730d 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
@@ -46,6 +46,7 @@ import {
lowerArrayFindLikeCall,
lowerArrayFlatMapCall,
lowerArrayReduceCall,
+ lowerTuplePredicateCall,
} from "./array-callbacks.js";
import {
lowerArraySpreadItems,
@@ -1847,12 +1848,19 @@ export function lowerTupleReadMethodCall(
if (lowerer.chainBlocked(access, call)) return null;
const method = access.name.text;
const search = method === "includes" || method === "indexOf" || method === "lastIndexOf";
+ const predicate =
+ method === "filter" ||
+ method === "find" ||
+ method === "findIndex" ||
+ method === "some" ||
+ method === "every";
if (
method !== "slice" &&
method !== "map" &&
method !== "flatMap" &&
method !== "join" &&
- !search
+ !search &&
+ !predicate
)
return null;
if (!lowerer.isStdlibMember(access)) return null;
@@ -2048,6 +2056,9 @@ export function lowerTupleReadMethodCall(
"capture the original tuple explicitly to preserve its identity",
);
}
+ if (predicate) {
+ return lowerTuplePredicateCall(lowerer, call, method, receiver, elem, fields.map(read));
+ }
const { fnArg, arity } = lowerArrayCallback(lowerer, callback, [elem], receiver.type);
const ret = fnArg.type.ret;
if (ret.kind === "void" || ret.kind === "func") lowerer.badType(call, lowerer.typeOf(call));
diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts
index 0ceb4d677..cf45a7575 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts
@@ -108,6 +108,7 @@ const PURE_MATH = new Set([
"math.exp",
"math.expm1",
"math.floor",
+ "math.f16round",
"math.fround",
"math.imul",
"math.log",
diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts
index b9400cbbd..7a19927de 100644
--- a/packages/compiler/src/frontend/lowering/containers/bytes.ts
+++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts
@@ -151,7 +151,7 @@ function lowerBytesSortCall(
/* ── typed arrays / Buffer ─────────────────────────────────────────────── */
/** The typed-array constructors with a runtime representation, by lib
- * interface name. Other flavors (Float16Array, BigInt64Array, ...) fall through
+ * interface name. Other flavors (BigInt64Array, BigUint64Array) fall through
* to the generic stdlib-constructor fence. DataView is handled separately. */
const BYTES_CTORS: Record = {
Uint8Array: "u8",
@@ -161,6 +161,7 @@ const BYTES_CTORS: Record = {
Int16Array: "i16",
Uint32Array: "u32",
Int32Array: "i32",
+ Float16Array: "f16",
Float32Array: "f32",
Float64Array: "f64",
};
@@ -397,6 +398,7 @@ function lowerArrayBufferView(
u8: "arrayBuffer.viewU8",
u32: "arrayBuffer.viewU32",
i32: "arrayBuffer.viewI32",
+ f16: "arrayBuffer.viewF16",
f32: "arrayBuffer.viewF32",
f64: "arrayBuffer.viewF64",
dv: "arrayBuffer.viewDV",
@@ -1220,6 +1222,7 @@ const DV_GETTERS: Record> — a
* literal-union key type makes a fixed-field record, which has no
* grouping representation) and stringifies number keys like JS property
- * keys. Node's result carries a null prototype — records here have no
- * prototype at all, so the difference is unobservable except through
- * util.inspect's "[Object: null prototype]" prefix (ledgered). Other
- * iterables fence (spread into an array first); Null when the callee
- * isn't the stdlib Object/Map groupBy. */
+ * keys; its result has a null prototype. Other iterables fence (spread
+ * into an array first). Null when the callee isn't the stdlib Object/Map
+ * groupBy. */
export function lowerGroupByStaticCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -50,15 +52,22 @@ export function lowerGroupByStaticCall(
lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call);
}
const items = lowerer.lowerExpr(call.arguments[0]!);
- if (items.type.kind !== "array") {
+ const elem = groupedElement(lowerer, items.type);
+ if (elem === null) {
lowerer.noLowering(
`${what} over an iterable of type '${lowerer.checker.typeToString(lowerer.typeOf(call.arguments[0]!))}'`,
call.arguments[0]!,
- "arrays are the supported items form — spread other iterables first: [...items]",
+ "arrays, Sets, Maps and generators are the supported items forms — spread other iterables first: [...items]",
);
}
- const elem = items.type.elem;
- const f = lowerer.lowerExpr(call.arguments[1]!);
+ const groupT = arrayOf(elem);
+ const fNode = call.arguments[1]!;
+ // An unannotated JavaScript callback parameter takes the element.
+ const f =
+ isJsSourceFile(fNode.getSourceFile()) &&
+ (ts.isArrowFunction(fNode) || ts.isFunctionExpression(fNode))
+ ? lowerArrayCallback(lowerer, fNode, [elem], groupT, true).fnArg
+ : lowerer.lowerExpr(fNode);
if (
f.type.kind !== "func" ||
f.type.params.length > 2 ||
@@ -81,7 +90,7 @@ export function lowerGroupByStaticCall(
if (
resultT?.kind !== "map" ||
!typeEquals(resultT.key, keyT) ||
- !typeEquals(resultT.value, items.type)
+ !typeEquals(resultT.value, groupT)
) {
lowerer.noLowering(
`${what} at this result type`,
@@ -89,7 +98,7 @@ export function lowerGroupByStaticCall(
"the result must be Map over the callback's K and the items' T — annotate: Map.groupBy(items, f)",
);
}
- const key = `map.groupBy:${typeKey(f.type)}`;
+ const key = `map.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`;
let helper = lowerer.mapHofHelpers.get(key);
if (!helper) {
helper = `%map.groupBy.${lowerer.mapHofHelpers.size}`;
@@ -124,9 +133,9 @@ export function lowerGroupByStaticCall(
}
const resultT: IrType & { kind: "record" } = {
kind: "record",
- shapeId: lowerer.shapes.intern([], false, lowerer.withUndefinedArm(items.type), []),
+ shapeId: lowerer.shapes.intern([], false, lowerer.withUndefinedArm(groupT), []),
};
- const key = `object.groupBy:${typeKey(f.type)}`;
+ const key = `object.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`;
let helper = lowerer.mapHofHelpers.get(key);
if (!helper) {
helper = `%object.groupBy.${lowerer.mapHofHelpers.size}`;
@@ -153,12 +162,13 @@ export function lowerGroupByStaticCall(
function buildGroupByFn(
lowerer: Lowerer,
name: string,
- itemsT: IrType & { kind: "array" },
+ sourceT: IrType,
fnT: IrType & { kind: "func" },
resultT: (IrType & { kind: "map" }) | (IrType & { kind: "record" }),
loc: SrcLoc,
): IrFunction {
- const elem = itemsT.elem;
+ const elem = groupedElement(lowerer, sourceT)!;
+ const itemsT = arrayOf(elem) as IrType & { kind: "array" };
const keyT = fnT.ret;
const arity = fnT.params.length;
const isMap = resultT.kind === "map";
@@ -325,10 +335,21 @@ function buildGroupByFn(
]),
{ kind: "return", value: gRef(), loc },
];
+ if (sourceT.kind !== "array") {
+ // Another iterable: the same per-element statements over its own
+ // walk, the callback's index counted by hand.
+ const loop = body[2] as IrStmt & { kind: "for" };
+ const perElement = loop.body.slice(1);
+ locals[0] = { id: "items.0", name: "items", type: sourceT, mutable: true };
+ const walk = groupedWalk(lowerer, sourceT, elem, perElement, loc, (local) =>
+ locals.push(local),
+ );
+ body.splice(1, 2, { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc }, ...walk);
+ }
return {
name,
params: [
- { localId: "items.0", name: "items", type: itemsT },
+ { localId: "items.0", name: "items", type: sourceT },
{ localId: "f.0", name: "f", type: fnT },
],
returnType: resultT,
@@ -337,3 +358,194 @@ function buildGroupByFn(
loc,
};
}
+
+/** The element a grouped source delivers: an array's or Set's element, a
+ * Map's [key, value] pair (the checker's own tuple shape), or a
+ * generator's yield. Null for other sources. */
+function groupedElement(lowerer: Lowerer, sourceT: IrType): IrType | null {
+ switch (sourceT.kind) {
+ case "array":
+ case "set":
+ return sourceT.elem;
+ case "map":
+ return {
+ kind: "record",
+ shapeId: lowerer.shapes.intern(
+ [
+ { name: "0", type: sourceT.key },
+ { name: "1", type: sourceT.value },
+ ],
+ true,
+ ),
+ };
+ case "generator":
+ return stepDrivable(sourceT) ? sourceT.yieldT : null;
+ default:
+ return null;
+ }
+}
+
+/** The walk of a Set, Map or generator source: binds `v.0` to each element,
+ * runs `perElement`, then advances the `i.0` counter. A Set or Map is
+ * walked live, as forEach walks it (appended entries are visited, deleted
+ * ones skipped), between iterEnter and iterExit. A generator is stepped,
+ * and closed when a callback throws, as Node closes the iterator. */
+function groupedWalk(
+ lowerer: Lowerer,
+ sourceT: IrType,
+ elem: IrType,
+ perElement: IrStmt[],
+ loc: SrcLoc,
+ addLocal: (local: IrLocal) => void,
+): IrStmt[] {
+ const items = (): IrExpr => varRef("items.0", sourceT, loc);
+ const bump: IrStmt = {
+ kind: "assign",
+ localId: "i.0",
+ value: {
+ kind: "bin",
+ op: "+",
+ left: varRef("i.0", F64, loc),
+ right: numLit(1, loc),
+ type: F64,
+ loc,
+ },
+ loc,
+ };
+ const visit = (value: IrExpr): IrStmt[] => [
+ { kind: "varDecl", localId: "v.0", init: value, loc },
+ ...perElement,
+ bump,
+ ];
+ if (sourceT.kind === "set" || sourceT.kind === "map") {
+ const collT = sourceT;
+ const iter = (
+ method: "iterCount" | "iterLive" | "iterKey" | "iterValue" | "iterEnter" | "iterExit",
+ args: IrExpr[],
+ type: IrType,
+ ): IrExpr =>
+ collT.kind === "map"
+ ? { kind: "mapIntrinsic", method, receiver: items(), args, type, loc }
+ : {
+ kind: "setIntrinsic",
+ method: method as "iterCount" | "iterLive" | "iterKey" | "iterEnter" | "iterExit",
+ receiver: items(),
+ args,
+ type,
+ loc,
+ };
+ addLocal({ id: "j.0", name: "j", type: F64, mutable: true });
+ const j = (): IrExpr => varRef("j.0", F64, loc);
+ const element: IrExpr =
+ collT.kind === "map"
+ ? {
+ kind: "recordLit",
+ fields: [
+ { name: "0", value: iter("iterKey", [j()], collT.key) },
+ { name: "1", value: iter("iterValue", [j()], collT.value) },
+ ],
+ type: elem,
+ loc,
+ }
+ : iter("iterKey", [j()], collT.elem);
+ return [
+ { kind: "exprStmt", expr: iter("iterEnter", [], VOID), loc },
+ {
+ kind: "tryCatch",
+ tryBody: [
+ {
+ kind: "for",
+ init: { kind: "varDecl", localId: "j.0", init: numLit(0, loc), loc },
+ cond: {
+ kind: "bin",
+ op: "<",
+ left: j(),
+ right: iter("iterCount", [], F64),
+ type: BOOL,
+ loc,
+ },
+ update: {
+ kind: "assign",
+ localId: "j.0",
+ value: { kind: "bin", op: "+", left: j(), right: numLit(1, loc), type: F64, loc },
+ loc,
+ },
+ body: [
+ {
+ kind: "if",
+ cond: iter("iterLive", [j()], BOOL),
+ then: visit(element),
+ else_: null,
+ loc,
+ },
+ ],
+ loc,
+ },
+ ],
+ catchBody: null,
+ catchLocalId: null,
+ finallyBody: [{ kind: "exprStmt", expr: iter("iterExit", [], VOID), loc }],
+ loc,
+ },
+ ];
+ }
+ // A generator: `open` holds while an element is being grouped, so a
+ // throwing callback closes the generator; an exhausted or throwing
+ // generator is not closed.
+ const genT = sourceT as GenType;
+ addLocal({ id: "open.0", name: "open", type: BOOL, mutable: true });
+ const setOpen = (value: boolean): IrStmt => ({
+ kind: "assign",
+ localId: "open.0",
+ value: { kind: "boolLit", value, type: BOOL, loc },
+ loc,
+ });
+ return [
+ {
+ kind: "varDecl",
+ localId: "open.0",
+ init: { kind: "boolLit", value: false, type: BOOL, loc },
+ loc,
+ },
+ {
+ kind: "tryCatch",
+ tryBody: [
+ {
+ kind: "while",
+ cond: { kind: "boolLit", value: true, type: BOOL, loc },
+ body: [
+ setOpen(false),
+ {
+ kind: "if",
+ cond: genStep(items(), loc),
+ then: [{ kind: "break", loc }],
+ else_: null,
+ loc,
+ },
+ setOpen(true),
+ ...visit(genValue(varRef("items.0", genT, loc) as IrExpr & { type: GenType }, loc)),
+ ],
+ loc,
+ },
+ ],
+ catchBody: null,
+ catchLocalId: null,
+ finallyBody: [
+ {
+ kind: "if",
+ cond: varRef("open.0", BOOL, loc),
+ then: [
+ {
+ kind: "exprStmt",
+ expr: { kind: "genResume", mode: "close", gen: items(), arg: null, type: VOID, loc },
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ },
+ ],
+ loc,
+ },
+ ];
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
index d9bf8bcd4..8c8349c0b 100644
--- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
@@ -41,6 +41,7 @@ import {
} from "../collection-ingestion.js";
import { strCharsCall } from "./array-construction.js";
import { arrayElementRead } from "./array-iteration.js";
+import { isSearchParamsIterable } from "../builtins/url.js";
/** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to
* mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop
@@ -1173,6 +1174,11 @@ export function lowerMapSeedNew(
mapT,
);
}
+ // new Map(sp) / new Map(sp.entries()): the checked-dynamic view's live
+ // pairs seed the map in order.
+ if (isSearchParamsIterable(lowerer, argNode)) {
+ return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT);
+ }
const input = collectionInput(lowerer, argNode);
if (input) {
if (!collectionDestinationMatches(lowerer, input, mapT)) return null;
diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
index bd89f6aea..c95731fc7 100644
--- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
@@ -1,3 +1,4 @@
+import { dateJsonType, isDateJsonSource } from "../builtins/dates.js";
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js";
import { pathModuleValue } from "../lower-builtin-values.js";
import { InternalCompilerError } from "../../../errors.js";
@@ -32,7 +33,7 @@ import { isSafeToRepeat } from "./evaluation-safety.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { fenceSymbolFieldCopy } from "../symbol-fields.js";
import { planUnionRetag } from "../union-retag.js";
-import { runtimeOptionalStorageOperand } from "../lower-exprs.js";
+import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js";
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
* contextual type when tsc has one (annotated declarations, arguments,
@@ -732,6 +733,10 @@ export function lowerObjectLiteral(
? prop.name
: null;
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
+ // A nested literal holding such a binding: building it as a static
+ // record would copy the binding's object.
+ if (value && (ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value)))
+ return jsHoldsDynObject(lowerer, value);
if (!value || !ts.isIdentifier(value)) return false;
const stored = lowerer.peekLocal(value)?.type ?? lowerer.globalOf(value)?.type;
const inferred = lowerer.mapTypeOf(lowerer.typeOf(value));
@@ -1151,6 +1156,30 @@ export function lowerObjectLiteral(
type = { kind: "record", shapeId: expected.shapeId };
}
}
+ // A Date's toJSON() result keeps its null in a field with no declared
+ // destination: the lib types say string.
+ if (!expected) {
+ for (const property of expr.properties) {
+ const value = ts.isPropertyAssignment(property)
+ ? property.initializer
+ : ts.isShorthandPropertyAssignment(property)
+ ? property.name
+ : null;
+ if (
+ value &&
+ property.name &&
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
+ isDateJsonSource(lowerer, value)
+ ) {
+ const widened = lowerer.runtimeOptionalRecordField(
+ type,
+ property.name.text,
+ dateJsonType(lowerer),
+ );
+ if (widened.kind === "record") type = { kind: "record", shapeId: widened.shapeId };
+ }
+ }
+ }
let shape = lowerer.shapes.get(type.shapeId)!;
// JavaScript Dates carry native object identity in checked storage. An
// inferred scalar Date field would erase that identity before boxing.
diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts
index 38c728027..d3c5eb1ba 100644
--- a/packages/compiler/src/frontend/lowering/function-returns.ts
+++ b/packages/compiler/src/frontend/lowering/function-returns.ts
@@ -59,6 +59,11 @@ export function appendImplicitUndefinedReturn(
): void {
if (bodyReturn.kind === "void") return;
if (!bodyMayFallThrough(body)) return;
+ // An async generator that falls off its end completes without awaiting
+ // anything: its request settles as the body returns, where `return x`
+ // takes a turn to await x.
+ let settled = false;
+ if (lowerer.ctx.isAsync === true && lowerer.ctx.generator) settled = true;
// A DYN body that can complete without returning (a JS function whose
// guarded return may not run — mustSucceed's `if (typeof fn ===
// 'function') return fn.apply(...)`): JS completes with undefined —
@@ -67,6 +72,7 @@ export function appendImplicitUndefinedReturn(
body.push({
kind: "return",
value: dynUndefinedExpr(loc),
+ ...(settled ? { awaited: true as const } : {}),
loc,
});
return;
@@ -74,7 +80,7 @@ export function appendImplicitUndefinedReturn(
if (bodyReturn.kind === "union") {
const value = lowerer.wrappedUndefined(bodyReturn, loc);
if (value) {
- body.push({ kind: "return", value, loc });
+ body.push({ kind: "return", value, ...(settled ? { awaited: true as const } : {}), loc });
return;
}
// no undefined arm: the trap below stands in, exactly like non-unions
diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts
index 712863072..bd785a256 100644
--- a/packages/compiler/src/frontend/lowering/generic-functions.ts
+++ b/packages/compiler/src/frontend/lowering/generic-functions.ts
@@ -94,6 +94,11 @@ export interface GenericFnInfo {
* checker types key an instantiation, exactly the generic machinery —
* the untyped params ARE the type parameters (see implicitCallInstance). */
implicitParams?: (ts.Symbol | null)[];
+ /** Implicit-any monomorphization of destructured object parameters
+ * (implicitPatternsOf): parallel to decl.parameters, the destructured
+ * names where the slot binds the call site's object type. Computed on
+ * first use. */
+ implicitPatterns?: (ImplicitPattern | null)[];
}
export interface GenericInstance {
@@ -132,6 +137,8 @@ export interface GenericInstance {
* any-params poisoned it) — the body lowers in return-INFERENCE mode
* (returnType holds the DYN recursion pin until the post-pass settles). */
implicitInferReturn?: true;
+ /** An erased instance (every type parameter bound to `unknown`). */
+ erased?: true;
}
/** Defaults bind during inference; collection only resolves symbols in
@@ -716,6 +723,7 @@ export function lowerGenericInstance(
lowerer.fnStack.push(fnCtx);
lowerer.localClassInstantiations.push({ owner: decl, name: inst.name });
lowerer.genericInstantiationPath = inst.path;
+ if (inst.erased) lowerer.erasingDepth++;
try {
// Static specializations bind lexical this only when the caller
// proved its exact receiver. Other instances retain the fence;
@@ -818,6 +826,7 @@ export function lowerGenericInstance(
lowerer.instantiationContext = prevContext;
lowerer.localClassInstantiations.pop();
lowerer.suppressStats = prevSuppress;
+ if (inst.erased) lowerer.erasingDepth--;
}
}
@@ -927,6 +936,29 @@ function runtimeOptionalHofGenericBinding(
return { typeParam, type: lowerer.runtimeOptionalType(receiver.elem) };
}
+/** The erased instance of a generic declaration: every type parameter
+ * bound to `unknown`, the function JavaScript runs for any type arguments.
+ * Values of generic methods (dynamic dispatch, interface views) call it. */
+export function erasedGenericInstance(
+ lowerer: Lowerer,
+ blame: ts.Node,
+ info: GenericFnInfo,
+): GenericInstance {
+ lowerer.erasingDepth++;
+ try {
+ const instance = genericValueInstance(
+ lowerer,
+ blame,
+ info,
+ new Map(info.typeParams.map((tp) => [tp, DYN])),
+ );
+ instance.erased = true;
+ return instance;
+ } finally {
+ lowerer.erasingDepth--;
+ }
+}
+
/** The instance a pinned VALUE reference names: the declaration's modes
* over the DECLARED types mapped under the bindings (mapType's resolver
* substitutes — the instance-body trick), which is the same result the
@@ -934,7 +966,7 @@ function runtimeOptionalHofGenericBinding(
* one instance per key. */
function genericValueInstance(
lowerer: Lowerer,
- ref: ts.Expression,
+ ref: ts.Node,
info: GenericFnInfo,
bindings: Map,
): GenericInstance {
@@ -1098,6 +1130,114 @@ function broadPromiseParam(lowerer: Lowerer, type: ts.Type): boolean {
);
}
+/** A destructured object parameter (`function f({ Base, name })`) whose
+ * names bind the call site argument's property types. */
+export interface ImplicitPattern {
+ elements: { property: string; symbol: ts.Symbol }[];
+}
+
+/** The destructured object parameters of a JS function-like that bind like
+ * implicit type parameters: every element a plain name (optionally renamed
+ * from an identifier or string key) with no default, no rest element, and
+ * no write in the body, under a broad inferred type. Parallel to
+ * decl.parameters. */
+export function implicitPatternsOf(
+ lowerer: Lowerer,
+ decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
+): (ImplicitPattern | null)[] {
+ return decl.parameters.map((param): ImplicitPattern | null => {
+ if (!ts.isObjectBindingPattern(param.name) || !decl.body) return null;
+ if (param.dotDotDotToken || param.questionToken || param.initializer) return null;
+ const elements: ImplicitPattern["elements"] = [];
+ for (const element of param.name.elements) {
+ if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name))
+ return null;
+ const key = element.propertyName ?? element.name;
+ if (!ts.isIdentifier(key) && !ts.isStringLiteral(key)) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(element.name);
+ if (!symbol || paramWrittenInBody(lowerer, decl.body, symbol, element.name.text)) return null;
+ // A JSDoc or contextual type names the properties: keep its ABI.
+ if ((lowerer.typeOf(element.name).flags & ts.TypeFlags.Any) === 0) return null;
+ elements.push({ property: key.text, symbol });
+ }
+ return elements.length > 0 ? { elements } : null;
+ });
+}
+
+/** The type a destructured parameter takes at one call site (the object's
+ * own representation, a record or a checked value) and the checker type
+ * each destructured name binds. An object literal argument types the names
+ * from its property expressions, which see the enclosing instance's
+ * bindings (`makeClass({ Base, name })` inside a specialized
+ * `extend(Base, name)`); any other argument uses its checker type. Null
+ * when the argument lacks a destructured property, or passes a class the
+ * callee itself defines: each such class would instantiate the callee
+ * again (a static `extend` calling `makeClass({ Base: this })`). */
+function implicitPatternBinding(
+ lowerer: Lowerer,
+ callee: ts.Node,
+ pattern: ImplicitPattern,
+ arg: ts.Expression,
+): { record: IrType; types: Map } | null {
+ let literal: ts.Expression = arg;
+ while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
+ const types = new Map();
+ if (ts.isObjectLiteralExpression(literal)) {
+ const values = new Map();
+ /** Shorthand properties name the binding they read. */
+ const shorthand = new Map();
+ for (const property of literal.properties) {
+ if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) {
+ values.set(property.name.text, property.name);
+ const read = lowerer.checker.getShorthandAssignmentValueSymbol(property);
+ if (read) shorthand.set(property.name.text, read);
+ } else if (
+ ts.isPropertyAssignment(property) &&
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
+ )
+ values.set(property.name.text, property.initializer);
+ else return null;
+ }
+ for (const element of pattern.elements) {
+ const value = values.get(element.property);
+ if (!value) return null;
+ const read = shorthand.get(element.property);
+ const type = (read && lowerer.implicitParamTypes?.get(read)) ?? lowerer.typeOf(value);
+ types.set(element.symbol, lowerer.checker.getBaseTypeOfLiteralType(type));
+ }
+ // The object keeps the representation the call site lowers it to; the
+ // destructured names read their static types back out of it.
+ const lowered = tryLowerExpression(lowerer, literal);
+ if (lowered?.type.kind !== "record" && lowered?.type.kind !== "dyn") return null;
+ if (lowered.type.kind === "record") {
+ for (const field of lowerer.shapes.get(lowered.type.shapeId)?.fields ?? [])
+ if (field.type.kind === "classval" && classDefinedIn(lowerer, field.type.className, callee))
+ return null;
+ }
+ return { record: lowered.type, types };
+ }
+ const argType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(arg));
+ const record = lowerer.mapTypeOf(argType);
+ if (record?.kind !== "record") return null;
+ for (const element of pattern.elements) {
+ const property = lowerer.checker.getPropertyOfType(argType, element.property);
+ if (!property) return null;
+ types.set(element.symbol, lowerer.checker.getTypeOfSymbol(property));
+ }
+ return { record, types };
+}
+
+/** The class is declared inside `scope`. */
+function classDefinedIn(lowerer: Lowerer, className: string, scope: ts.Node): boolean {
+ for (
+ let n: ts.Node | undefined = lowerer.classes.get(className)?.decl ?? undefined;
+ n;
+ n = n.parent
+ )
+ if (n === scope) return true;
+ return false;
+}
+
/** The implicit-type-parameter slots of a JS function-like: parallel to
* decl.parameters, the param SYMBOL where the slot is a bindable
* implicit param (identifier-named, broad inferred/JSDoc type, not
@@ -1142,6 +1282,10 @@ export function implicitAnyParamSymbolsOf(
hasImplicitParams = true;
return sym;
});
+ // Destructured object parameters monomorphize too (implicitPatternsOf);
+ // their slots stay null here.
+ if (!hasImplicitParams && isJsSourceFile(decl.getSourceFile()))
+ hasImplicitParams = implicitPatternsOf(lowerer, decl).some((pattern) => pattern !== null);
return hasImplicitParams ? out : null;
}
@@ -1413,7 +1557,27 @@ export function implicitCallInstance(
): GenericInstance {
const shapes: ParamShape[] = [];
const argTypes = new Map();
+ const patterns = (info.implicitPatterns ??= implicitPatternsOf(lowerer, info.decl));
+ /** The destructured names' bound types, which the object's own
+ * representation does not always carry. */
+ const patternKey: string[] = [];
info.decl.parameters.forEach((param, i) => {
+ const pattern = patterns[i];
+ const patternArg = call.arguments[i];
+ if (pattern && patternArg && !ts.isSpreadElement(patternArg)) {
+ // The argument's object type binds each destructured name, so a
+ // class passed as `{ Base }` keeps its static class type.
+ const bound = implicitPatternBinding(lowerer, info.decl, pattern, patternArg);
+ if (bound) {
+ for (const [symbol, type] of bound.types) {
+ argTypes.set(symbol, type);
+ const mapped = lowerer.mapTypeOf(type);
+ patternKey.push(mapped ? typeKey(mapped) : "?");
+ }
+ shapes.push({ type: bound.record, mode: "required" });
+ return;
+ }
+ }
const sym = info.implicitParams![i];
if (!sym) {
shapes.push(lowerer.paramShape(param));
@@ -1431,6 +1595,17 @@ export function implicitCallInstance(
shapes.push({ type: bound, mode: "required" });
return;
}
+ // `this` in a static method specialized for its receiver class is
+ // that class: bind it as a call naming the class would.
+ if (stored?.kind === "classval" && arg.kind === ts.SyntaxKind.ThisKeyword) {
+ const classDecl = lowerer.classes.get(stored.className)?.decl;
+ const classType = classDecl?.name ? lowerer.typeOf(classDecl.name) : null;
+ if (classType) {
+ argTypes.set(sym, classType);
+ shapes.push({ type: stored, mode: "required" });
+ return;
+ }
+ }
// A represented native array must keep its storage when a direct
// inferred helper receives it. Re-boxing a checker-any array would
// copy it, losing writes (and attempting to read unfilled slots).
@@ -1540,7 +1715,7 @@ export function implicitCallInstance(
}
shapes.push({ type: bound, mode: "required" });
});
- return internImplicitInstance(lowerer, call, info, shapes, argTypes);
+ return internImplicitInstance(lowerer, call, info, shapes, argTypes, patternKey.join(","));
}
/** The all-dyn DEFAULT instance — today's compiled body exactly: what a
@@ -1573,8 +1748,9 @@ function internImplicitInstance(
info: GenericFnInfo,
shapes: ParamShape[],
argTypes: Map,
+ patternKey = "",
): GenericInstance {
- const key = shapes.map((s) => typeKey(s.type)).join(",");
+ const key = shapes.map((s) => typeKey(s.type)).join(",") + (patternKey ? `|${patternKey}` : "");
let inst = info.instances.get(key);
if (inst?.implicitState === "failed")
lowerer.unsupported("SC1090", blame, `the failed specialization of '${info.baseName}'`);
diff --git a/packages/compiler/src/frontend/lowering/js-class-fields.ts b/packages/compiler/src/frontend/lowering/js-class-fields.ts
new file mode 100644
index 000000000..4df513c91
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/js-class-fields.ts
@@ -0,0 +1,182 @@
+import * as ts from "../ts7/adapter.js";
+import { isClassOwnEnumerableFieldName } from "../../ir/ir.js";
+import type { ClassInfo } from "./lower-classes.js";
+
+/** Reflection that can remove a property or change its attributes. */
+const PROPERTY_REFLECTION = new Set(["deleteProperty", "freeze", "seal", "preventExtensions"]);
+
+/** `Object.freeze({ ... })` and the like apply to a new literal, which is
+ * not a class instance. */
+function freshLiteralArgument(callee: ts.PropertyAccessExpression): boolean {
+ const call = callee.parent;
+ if (!ts.isCallExpression(call) || call.expression !== callee) return false;
+ let argument = call.arguments[0];
+ while (argument && ts.isParenthesizedExpression(argument)) argument = argument.expression;
+ return (
+ argument !== undefined &&
+ (ts.isObjectLiteralExpression(argument) || ts.isArrayLiteralExpression(argument))
+ );
+}
+
+/** Property names that a `delete` in `files` can remove, or null when a
+ * delete with a computed key or a reflective removal can remove any name. */
+export function deletedPropertyNames(files: readonly ts.SourceFile[]): Set | null {
+ const names = new Set();
+ let any = false;
+ for (const file of files)
+ ts.walkPreorder(file, (node) => {
+ if (any) return "stop";
+ if (
+ ts.isPropertyAccessExpression(node) &&
+ PROPERTY_REFLECTION.has(node.name.text) &&
+ !freshLiteralArgument(node)
+ ) {
+ any = true;
+ return "stop";
+ }
+ if (!ts.isDeleteExpression(node)) return undefined;
+ let target = node.expression;
+ while (ts.isParenthesizedExpression(target)) target = target.expression;
+ if (ts.isPropertyAccessExpression(target)) names.add(target.name.text);
+ else if (
+ ts.isElementAccessExpression(target) &&
+ (ts.isStringLiteralLike(target.argumentExpression) ||
+ ts.isNumericLiteral(target.argumentExpression))
+ )
+ names.add(target.argumentExpression.text);
+ else any = true;
+ return undefined;
+ });
+ return any ? null : names;
+}
+
+/** Whether `node` reads or passes `this` or `super`. */
+function usesReceiver(node: ts.Node): boolean {
+ let found = false;
+ ts.walkPreorder(node, (part) => {
+ if (part.kind === ts.SyntaxKind.ThisKeyword || part.kind === ts.SyntaxKind.SuperKeyword) {
+ found = true;
+ return "stop";
+ }
+ return undefined;
+ });
+ return found;
+}
+
+/** The field a statement `this.name = value` creates when `value` does not
+ * use the receiver, or null for any other statement. */
+function receiverFreeFieldAssignment(statement: ts.Statement): string | null {
+ if (!ts.isExpressionStatement(statement)) return null;
+ const assignment = statement.expression;
+ if (
+ !ts.isBinaryExpression(assignment) ||
+ assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ !ts.isPropertyAccessExpression(assignment.left) ||
+ assignment.left.expression.kind !== ts.SyntaxKind.ThisKeyword ||
+ usesReceiver(assignment.right)
+ )
+ return null;
+ return assignment.left.name.text;
+}
+
+export interface Construction {
+ /** Fields created before any code could observe the instance. */
+ created: Set;
+ /** No code could observe the instance before construction finished. */
+ sealed: boolean;
+}
+
+/** The fields a class's construction creates before the instance can be
+ * observed: field declarations and top-level `this.name = value`
+ * statements whose values do not use the receiver, up to the first
+ * statement that uses `this` or `super` in any other way. A subclass
+ * continues its base's sequence (`base`) only when the whole base
+ * construction kept the instance unobserved. `isField` tells fields from
+ * names whose assignment runs a setter or adds a property. Null when the
+ * constructor's shape is not analyzed. */
+export function constructionPrefix(
+ decl: ts.ClassDeclaration | ts.ClassExpression,
+ base: Construction | null,
+ isField: (name: string) => boolean,
+): Construction | null {
+ const created = new Set(base?.created ?? []);
+ let sealed = base?.sealed ?? true;
+ const ctor = decl.members.find(
+ (member): member is ts.ConstructorDeclaration =>
+ ts.isConstructorDeclaration(member) && member.body !== undefined,
+ );
+ if (ctor?.parameters.some((parameter) => usesReceiver(parameter))) sealed = false;
+ const statements = ctor?.body?.statements ?? [];
+ // A derived constructor's statements before super() cannot reach the
+ // instance; the field declarations run when super() returns.
+ let at = 0;
+ if (base && ctor) {
+ const superAt = statements.findIndex(
+ (statement) =>
+ ts.isExpressionStatement(statement) &&
+ ts.isCallExpression(statement.expression) &&
+ statement.expression.expression.kind === ts.SyntaxKind.SuperKeyword,
+ );
+ if (superAt < 0) return null;
+ const call = (statements[superAt] as ts.ExpressionStatement).expression as ts.CallExpression;
+ if (call.arguments.some(usesReceiver)) sealed = false;
+ at = superAt + 1;
+ }
+ for (const member of decl.members) {
+ if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name)) continue;
+ if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))
+ continue;
+ if (member.initializer && usesReceiver(member.initializer)) sealed = false;
+ if (sealed) created.add(member.name.text);
+ }
+ for (const statement of statements.slice(at)) {
+ if (!sealed) break;
+ // A name that is not a field runs a setter or adds a property bag
+ // entry, either of which can observe the instance.
+ const name = receiverFreeFieldAssignment(statement);
+ if (name !== null && isField(name)) created.add(name);
+ else if (name !== null || usesReceiver(statement) || ts.isReturnStatement(statement))
+ sealed = false;
+ }
+ return { created, sealed };
+}
+
+/** Whether a class declares a getter or setter, which can intercept a
+ * base constructor's field assignment. */
+function declaresAccessor(info: ClassInfo): boolean {
+ return [...info.methods.keys()].some(
+ (name) => name.startsWith("get:") || name.startsWith("set:"),
+ );
+}
+
+/** `constructionPrefix` for a collected class and its bases. */
+export function construction(info: ClassInfo): Construction | null {
+ if (!info.decl) return null;
+ let base: Construction | null = null;
+ if (info.base) {
+ base = construction(info.base);
+ if (!base) return null;
+ if (declaresAccessor(info)) base = { created: base.created, sealed: false };
+ }
+ return constructionPrefix(info.decl, base, (name) => info.fields.has(name));
+}
+
+/** Whether every class in the family under `root` creates all of its
+ * enumerable fields before its instances can be observed, and no field
+ * name can be deleted. Such fields exist for the instance's whole life in
+ * layout order, as TypeScript fields do, so their writes need no creation
+ * tracking. */
+export function fixedFieldFamily(root: ClassInfo, deleted: Set | null): boolean {
+ if (deleted === null) return false;
+ const fixed = (info: ClassInfo): boolean => {
+ if (declaresAccessor(info)) return false;
+ const built = construction(info);
+ if (!built) return false;
+ for (const field of info.fieldOrder) {
+ if (!isClassOwnEnumerableFieldName(field.name)) continue;
+ if (!built.created.has(field.name) || deleted.has(field.name)) return false;
+ }
+ return info.subclasses.every(fixed);
+ };
+ return fixed(root);
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts
index 0bccde3fd..9d434eae1 100644
--- a/packages/compiler/src/frontend/lowering/lower-assert.ts
+++ b/packages/compiler/src/frontend/lowering/lower-assert.ts
@@ -2364,6 +2364,7 @@ const BYTES_BRANDS: ReadonlySet = new Set([
"Int8Array",
"Int16Array",
"Int32Array",
+ "Float16Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index 84f634ad9..56c639397 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -13,6 +13,7 @@ import {
lowerTimersMemberCall,
TIMER_MODULE_MEMBERS,
} from "./lower-timers.js";
+import { lowerMockTimersElementCall } from "./lower-test.js";
import { objectEnumerationReceiver } from "./object-enumeration-receiver.js";
import { InternalCompilerError } from "../../errors.js";
import { isFsStreamMember, lowerFsStreamMethodCall } from "./builtins/fs-streams.js";
@@ -105,6 +106,8 @@ import {
lowerRequireResolveCall,
} from "./builtins/module-resolution.js";
import { lowerCipherMethodCall, lowerCryptoHashMethodCall } from "./builtins/crypto.js";
+import { lowerErrorIsErrorCall } from "./builtins/errors.js";
+import { lowerDisposableStackUseCall } from "./builtins/disposable-stack.js";
import {
lowerDirentMethodCall,
lowerFileHandleMethodCall,
@@ -1378,6 +1381,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.fenceStaticResponseMember(expr.expression, "call") ??
lowerer.fenceStaticHeadersMember(expr.expression, "call") ??
lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ??
+ lowerDisposableStackUseCall(lowerer, expr, expr.expression) ??
lowerer.lowerStdlibMethodCall(expr, expr.expression) ??
// Dyn receivers (JSON.parse-derived `unknown`/`any` values) —
@@ -1387,6 +1391,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// by the receiver's own element.
lowerer.lowerFilterNarrowCall(expr, expr.expression) ??
lowerArrayIsArrayCall(lowerer, expr, expr.expression) ??
+ lowerErrorIsErrorCall(lowerer, expr, expr.expression) ??
lowerBigIntMethodCall(lowerer, expr, expr.expression) ??
lowerSymbolStaticCall(lowerer, expr, expr.expression) ??
lowerSymbolMethodCall(lowerer, expr, expr.expression) ??
@@ -1517,6 +1522,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// The element spelling of an unsupported native Web method must fence
// before lowering the callee into a checked-dynamic keyed read.
if (ts.isElementAccessExpression(expr.expression)) {
+ const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression);
+ if (timersDispose) return timersDispose;
const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr);
if (symbolMethod) return symbolMethod;
lowerer.fenceUnsupportedFetchConstructorMember(expr.expression);
@@ -3199,6 +3206,13 @@ function lowerDynReceiverMethodCall(
const info = lowerer.classes.get(probed.type.className);
if (info) return lowerStaticMethodCall(lowerer, call, access);
}
+ // A possibly missing element of a class-typed array (`items[i]`) is
+ // the receiver's class or undefined. The class method call checks for
+ // undefined itself, as it does in TypeScript.
+ const presentArm =
+ probed?.type.kind === "union" ? lowerer.stripUndefinedArm(probed.type) : null;
+ if (presentArm?.kind === "object" && typeEquals(presentArm, lowerer.mapTypeOf(recvTs) ?? DYN))
+ return null;
if (
probed?.type.kind === "union" &&
isJsSourceFile(call.getSourceFile()) &&
@@ -3378,6 +3392,18 @@ function lowerDynReceiverMethodCall(
loc: locOf(call),
};
}
+ // Every dyn kind's prototype chain declares valueOf, and the runtime
+ // answers each kind's own: primitives and plain objects themselves, a
+ // Date its time value, an own or inherited valueOf member its result.
+ if (access.name.text === "valueOf" && call.arguments.length === 0) {
+ return {
+ kind: "libCall",
+ fn: "dyn.valueOf",
+ args: [recv, { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }],
+ type: DYN,
+ loc: locOf(call),
+ };
+ }
// SHARED prototype names with a runtime dispatch (scr_dyn_invoke):
// push/slice/join/forEach/map/apply/... dispatch on the receiver's
// RUNTIME kind — the honest answer for names more than one dyn-
@@ -4559,6 +4585,24 @@ function reconcileOverloadReturn(
) {
call = lowerer.maybeNarrow(call, expr);
}
+ // An override narrowed to part of its base's union return shares the
+ // union ABI; a call resolved to that override narrows the result back.
+ if (
+ call.type.kind === "union" &&
+ rsig &&
+ rdecl &&
+ ts.isMethodDeclaration(rdecl) &&
+ lowerer.unionReturnOverrides.has(rdecl)
+ ) {
+ const declared = lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(rsig));
+ if (declared && !typeEquals(declared, call.type)) {
+ if (declared.kind !== "union") {
+ const helper = lowerer.narrowedArmHelper(call.type.unionId, declared, call.loc);
+ if (helper) return lowerer.checkedNarrowCall(helper, call, declared, call.loc);
+ }
+ return lowerer.coerceInto(expr, call, declared);
+ }
+ }
const resolvedOverload =
rsig &&
rdecl &&
@@ -8169,7 +8213,7 @@ export function lowerRecordFieldCall(
}
}
const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call));
- const result: IrExpr = {
+ let result: IrExpr = {
kind: "callValue",
callee,
...(receiver ? { receiver } : {}),
@@ -8177,6 +8221,10 @@ export function lowerRecordFieldCall(
type: callee.type.ret,
loc,
};
+ // A generic member's field holds its erased instance: the result
+ // converts to this call's instantiated return type at the call.
+ if (isGenericCallableMemberType(lowerer.typeOf(access), lowerer.checker))
+ result = lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result;
}
@@ -8590,6 +8638,26 @@ function lowerObjLitGenericMethodCall(
const onInterface = lowerer.checker
.declarationsOf(propSym)
.some((d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent));
+ // A receiver held as a checked-dynamic value (a class-backed interface)
+ // dispatches by name to the runtime object's member, which a generic
+ // method answers with its erased instance; the result converts to this
+ // call's instantiated return type.
+ if (onInterface && !call.arguments.some(ts.isSpreadElement)) {
+ const receiver = lowerer.lowerExpr(access.expression);
+ if (receiver.type.kind === "dyn") {
+ const loc = locOf(call);
+ const invoked: IrExpr = {
+ kind: "dynInvoke",
+ recv: receiver,
+ method: name,
+ calleeName: access.getText(),
+ args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
+ type: DYN,
+ loc,
+ };
+ return lowerer.coerceInto(call, invoked, lowerer.irTypeOf(call));
+ }
+ }
if (onInterface) {
lowerer.unsupported(
"SC1090",
@@ -8733,7 +8801,15 @@ export function lowerObjectMethodCall(
// The EventEmitter surface: API-named calls on emitter-rooted
// receivers lower through the emitter spoke (subclass members with
// these names are fenced at collection, so `found` never shadows).
- if (info && !found && EMITTER_API_MEMBERS.has(access.name.text) && emitterRooted(lowerer, info)) {
+ if (
+ info &&
+ !found &&
+ (EMITTER_API_MEMBERS.has(access.name.text) ||
+ // A static with no instance twin (events.getEventListeners).
+ (access.name.text === "getEventListeners" &&
+ lowerer.checker.getConstructSignatures(lowerer.typeOf(access.expression)).length > 0)) &&
+ emitterRooted(lowerer, info)
+ ) {
return lowerEmitterMethodCall(lowerer, call, access, info);
}
// The inherited EventTarget/Event methods of a class rooted at those
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 4577ae452..06739166c 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -53,7 +53,12 @@ import {
} from "./instantiation-path.js";
import { appendImplicitUndefinedReturn, hasExplicitJsDocReturn } from "./function-returns.js";
import { bodyReadsArguments } from "../arguments-usage.js";
-import { generatorMeta, type ParamShape } from "./call-signatures.js";
+import {
+ checkedJsDocumentedParameter,
+ documentedParameter,
+ generatorMeta,
+ type ParamShape,
+} from "./call-signatures.js";
import { omittedArgFor } from "./call-arguments.js";
import {
isGenericCallableMemberType,
@@ -74,11 +79,15 @@ import { isAssertClassNew } from "./assert-instances.js";
import { lowerArrayConstructor } from "./containers/array-construction.js";
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
-import { bufEncoding } from "./containers/bytes.js";
+import { knownBufEncoding } 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 { lowerMessageChannelNew, lowerWorkerNew } from "./builtins/workers.js";
+import {
+ lowerBroadcastChannelNew,
+ lowerMessageChannelNew,
+ lowerWorkerNew,
+} from "./builtins/workers.js";
import {
unsupportedPackageValueDiag,
unsupportedDiag,
@@ -129,6 +138,7 @@ import { hasLexicalPrivateReference, hasStaticThis, rejectStaticThis } from "./s
import { lowerUrlNew } from "./lower-url.js";
import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js";
import { classStaticDataFor } from "./class-static-data.js";
+import { construction, constructionPrefix } from "./js-class-fields.js";
import { lowerObjectFactoryNew } from "./object-factory-new.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
@@ -386,6 +396,9 @@ export interface ClassInfo {
def: IrClassDef;
expressionInitializationEmitted?: true;
runtimeStatics?: readonly ts.ClassElement[];
+ /** Direct calls of runtime static methods (directRuntimeStaticCall), by
+ * member name. */
+ runtimeStaticCalls?: Map;
runtimePrototypeMembers?: readonly ts.ClassElement[];
localPrototypeData?: true;
prototypeMethodValues?: Map;
@@ -972,7 +985,8 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E
.some(
(decl) =>
ts.isParameter(decl) &&
- (ts.isConstructorDeclaration(decl.parent) ||
+ ((ts.isConstructorDeclaration(decl.parent) &&
+ !checkedJsDocumentedParameter(lowerer, decl)) ||
(lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0),
)
)
@@ -2989,7 +3003,14 @@ export function collectClassShapeInner(
"overriding a method with a different signature (parameter types must match the base declaration exactly; returns may narrow to a subclass)",
);
}
- methods.set(mName, { params: shapes, ret, abstract: true });
+ const unionReturn =
+ overridden?.sig.ret.kind === "union" && !typeEquals(ret, overridden.sig.ret);
+ if (unionReturn) lowerer.unionReturnOverrides.add(member);
+ methods.set(mName, {
+ params: shapes,
+ ret: unionReturn ? overridden.sig.ret : ret,
+ abstract: true,
+ });
continue;
}
// A body-less method is an OVERLOAD SIGNATURE (abstract methods
@@ -3256,6 +3277,12 @@ export function collectClassShapeInner(
);
}
}
+ // A return narrowed to part of an inherited union returns through
+ // the union ABI; call sites narrow it back to the declared type.
+ if (overridden?.sig.ret.kind === "union" && !typeEquals(ft.ret, overridden.sig.ret)) {
+ ft.ret = overridden.sig.ret;
+ lowerer.unionReturnOverrides.add(member);
+ }
const asyncMember =
member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true;
// Async methods dispatch STATICALLY (the body enters through its
@@ -3653,6 +3680,19 @@ export function collectClassShapeInner(
const keySym = lateBoundKeySymOf(lowerer, p);
if (keySym) lateBoundByKey.set(keySym, p);
}
+ // A subclass field created before anything can observe the instance
+ // keeps its type: super() cannot run code that reads it earlier.
+ const baseConstruction =
+ base && ![...methods.keys()].some((m) => m.startsWith("get:") || m.startsWith("set:"))
+ ? construction(base)
+ : null;
+ const sealedPrefix = baseConstruction
+ ? constructionPrefix(
+ decl,
+ baseConstruction,
+ (name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
+ )
+ : null;
if (ctor?.body) {
for (const stmt of ctor.body.statements) {
const lhs =
@@ -3723,7 +3763,8 @@ export function collectClassShapeInner(
type = inheritedJsFieldType(lowerer, decl, name, type);
// super() can invoke a subclass setter or method before its
// constructor assignments run. These slots start undefined.
- if (base && lowerer.dynConvertible(type)) type = DYN;
+ if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
+ type = DYN;
fields.set(name, type);
fieldOrder.push({ name, type, initializer: undefined });
continue;
@@ -5642,6 +5683,8 @@ function staticCallOn(
throughValue: boolean,
): IrExpr | null {
const loc = locOf(call);
+ const runtimeStatic = directRuntimeStaticCall(lowerer, call, access, info, throughValue);
+ if (runtimeStatic) return runtimeStatic;
if (
(hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field) ||
(info.callableBase &&
@@ -5780,6 +5823,179 @@ function staticCallOn(
return { kind: "call", callee: fnName, args, type: found.method.ret, loc };
}
+/** `C.m(...)` where `m` is a static method that a function-local
+ * JavaScript class defines at runtime (a factory's `class extends base`),
+ * inherited by `C` through its statically known base chain. When the body
+ * captures nothing from the factory's scope, every evaluation of the class
+ * defines the same method, so the call lowers directly with `this` bound
+ * to `C`, as a free function given `C` would. A receiver-dependent result
+ * (a class built from `this`) then keeps a static base. Null when another
+ * declaration of `m` answers first, the program assigns that member, or
+ * the method form has no direct lowering. */
+function directRuntimeStaticCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ info: ClassInfo,
+ throughValue: boolean,
+): IrExpr | null {
+ const name = access.name.text;
+ if (throughValue || name.startsWith("#")) return null;
+ let declarer: ClassInfo | null = null;
+ let member: ts.MethodDeclaration | null = null;
+ for (let c: ClassInfo | null = info; c; c = c.base) {
+ if (
+ c.staticFields.some((field) => field.name === name) ||
+ c.staticMethods?.has(name) === true ||
+ c.genericStatics?.has(name) === true
+ )
+ return null;
+ const runtime = c.runtimeStatics?.find(
+ (element) =>
+ (element as { name?: ts.PropertyName }).name !== undefined &&
+ ts.isIdentifier((element as { name: ts.PropertyName }).name) &&
+ (element as { name: ts.Identifier }).name.text === name,
+ );
+ if (runtime) {
+ if (!ts.isMethodDeclaration(runtime) || !runtime.body) return null;
+ declarer = c;
+ member = runtime;
+ break;
+ }
+ }
+ if (!declarer || !member || !declarer.decl) return null;
+ if (member.asteriskToken || member.typeParameters) return null;
+ if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword))
+ return null;
+ if (hasLexicalSuper(member) || capturesEnclosingScope(lowerer, member, declarer.decl))
+ return null;
+ if (memberAssigned(lowerer, member, name) || bodyReadsArguments(member)) return null;
+ const implicit =
+ member.parameters.length === 0 ? null : implicitAnyParamSymbolsOf(lowerer, member, true);
+ if (member.parameters.length > 0 && !implicit) return null;
+ let calls = declarer.runtimeStaticCalls;
+ if (!calls) {
+ calls = new Map();
+ declarer.runtimeStaticCalls = calls;
+ }
+ const method: GenericFnInfo = calls.get(name) ?? {
+ decl: member,
+ baseName: name,
+ qualifiedName: `%${declarer.def.name}.runtimeStatic:${name}`,
+ typeParams: [],
+ instances: new Map(),
+ member: { cls: declarer, kind: "static" },
+ ...(implicit ? { implicitParams: implicit } : {}),
+ };
+ calls.set(name, method);
+ // A direct lowering the method form does not support keeps the dynamic
+ // call: its refusal is not this program's error.
+ const sink: ScrDiagnostic[] = [];
+ const previousSink = lowerer.diagSink;
+ lowerer.diagSink = sink;
+ let lowered: IrExpr;
+ try {
+ const receiver = staticMethodForReceiver(lowerer, access, method, info, false);
+ const instance = implicit
+ ? implicitCallInstance(lowerer, call, receiver)
+ : genericCallInstance(lowerer, call, receiver);
+ lowered = {
+ kind: "call",
+ callee: instance.name,
+ args: lowerer.completeArgs(call.arguments, instance.params, locOf(call), call),
+ type: instance.returnType,
+ loc: locOf(call),
+ };
+ } catch (error) {
+ if (error instanceof PoisonError) return null;
+ throw error;
+ } finally {
+ lowerer.diagSink = previousSink;
+ }
+ return sink.length === 0 ? lowered : null;
+}
+
+/** `super` inside the method (arrows included) names the defining
+ * class's base, which a receiver specialization does not track. */
+function hasLexicalSuper(root: ts.Node): boolean {
+ let found = false;
+ const visit = (node: ts.Node): void => {
+ if (node.kind === ts.SyntaxKind.SuperKeyword) found = true;
+ if (
+ (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) ||
+ ts.isClassDeclaration(node) ||
+ ts.isClassExpression(node)
+ )
+ return;
+ node.forEachChild(visit);
+ };
+ root.forEachChild(visit);
+ return found;
+}
+
+/** The method reads a binding declared in a function that encloses its
+ * class: each evaluation of the class can see a different value. */
+function capturesEnclosingScope(
+ lowerer: Lowerer,
+ member: ts.MethodDeclaration,
+ classDecl: ts.ClassLikeDeclaration,
+): boolean {
+ let scope: ts.Node | undefined = classDecl.parent;
+ while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent;
+ if (!scope || ts.isSourceFile(scope)) return false;
+ const inside = (node: ts.Node, container: ts.Node): boolean => {
+ for (let n: ts.Node | undefined = node; n; n = n.parent) if (n === container) return true;
+ return false;
+ };
+ let captured = false;
+ const visit = (node: ts.Node): void => {
+ if (captured) return;
+ if (ts.isIdentifier(node)) {
+ const symbol = lowerer.checker.getSymbolAtLocation(node);
+ const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
+ // The enclosing function's own name is the same binding for every
+ // evaluation.
+ if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true;
+ return;
+ }
+ node.forEachChild(visit);
+ };
+ member.forEachChild(visit);
+ return captured;
+}
+
+/** Some code assigns or deletes a member of this name, or defines it by
+ * reflection: the member a runtime lookup finds can then differ. */
+function memberAssigned(lowerer: Lowerer, member: ts.MethodDeclaration, name: string): boolean {
+ let found = false;
+ for (const sf of lowerer.program.getSourceFiles()) {
+ if (sf.isDeclarationFile || found) continue;
+ if (!sf.text.includes(name)) continue;
+ ts.walkPreorder(sf, (node) => {
+ if (found) return "skip";
+ if (ts.isPropertyAccessExpression(node) && node.name.text === name) {
+ const parent = node.parent;
+ if (
+ parent &&
+ ((ts.isBinaryExpression(parent) &&
+ parent.left === node &&
+ parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
+ parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
+ ts.isDeleteExpression(parent))
+ )
+ found = true;
+ }
+ if (ts.isStringLiteral(node) && node.text === name && node !== member.name) {
+ const parent = node.parent;
+ if (parent && (ts.isCallExpression(parent) || ts.isElementAccessExpression(parent)))
+ found = true;
+ }
+ return undefined;
+ });
+ }
+ return found;
+}
+
function staticMethodForReceiver(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
@@ -5955,6 +6171,22 @@ function overrideReturnAbi(
className: string,
base: ClassInfo | null,
): IrType {
+ // A return narrowed to part of an inherited union (`Meta` for
+ // `Meta | undefined`) keeps the union ABI; returns convert into it.
+ if (inherited.kind === "union" && !typeEquals(declared, inherited)) {
+ const arms = lowerer.unions.get(inherited.unionId)?.arms ?? [];
+ const fits = (type: IrType): boolean =>
+ arms.some(
+ (arm) =>
+ typeEquals(arm, type) ||
+ (arm.kind === "object" &&
+ type.kind === "object" &&
+ lowerer.isSubclassOf(type.className, arm.className)),
+ );
+ const parts =
+ declared.kind === "union" ? (lowerer.unions.get(declared.unionId)?.arms ?? []) : [declared];
+ return parts.length > 0 && parts.every(fits) ? inherited : declared;
+ }
if (declared.kind !== "object" || inherited.kind !== "object") return declared;
if (
lowerer.isSubclassOf(declared.className, inherited.className) ||
@@ -6222,7 +6454,8 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) {
if (
ts.isMethodDeclaration(declaration) &&
- isJsSourceFile(declaration.getSourceFile())
+ isJsSourceFile(declaration.getSourceFile()) &&
+ !checkedJsOverride(lowerer, declaration, member)
) {
lowerer.virtualJsMethods.add(declaration);
if (
@@ -6292,6 +6525,21 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
}
}
+/** An override whose base and overriding methods are in files TypeScript
+ * checks and document every parameter: TypeScript checks the override
+ * against its base, so both keep their declared signatures, as in
+ * TypeScript classes. */
+function checkedJsOverride(
+ lowerer: Lowerer,
+ base: ts.MethodDeclaration,
+ member: ts.MethodDeclaration,
+): boolean {
+ return [base, member].every(
+ (method) =>
+ lowerer.checkedJsFile(method.getSourceFile()) && method.parameters.every(documentedParameter),
+ );
+}
+
/** The receiver's EXACT runtime class, when the expression proves it: a
* `new C(...)` expression directly, or a const binding initialized with
* one (the binding can never be reassigned to a subclass instance).
@@ -6740,7 +6988,8 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri
member === "iterator" ||
member === "asyncIterator" ||
member === "dispose" ||
- member === "asyncDispose"
+ member === "asyncDispose" ||
+ member === "toPrimitive"
) {
return `sym:${member}`;
}
@@ -7612,7 +7861,6 @@ export function superCallStmt(
args: IrExpr[],
loc: SrcLoc,
): IrStmt {
- const base = superBaseOf(info)!;
const thisRef: IrExpr = {
kind: "varRef",
localId: thisLocal.id,
@@ -7657,6 +7905,8 @@ export function superCallStmt(
},
loc,
};
+ // A callable base has no class info, so the base is read after that case.
+ const base = superBaseOf(info)!;
if (base.builtinError) {
// super(message) into the runtime-provided Error constructor: stamps
// name/message on the (already-allocated) object. Receiver + message
@@ -8153,7 +8403,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
if (worker) return worker;
const readlineUi = lowerReadlineUiNew(lowerer, expr);
if (readlineUi) return readlineUi;
- const channel = lowerMessageChannelNew(lowerer, expr);
+ const channel = lowerMessageChannelNew(lowerer, expr) ?? lowerBroadcastChannelNew(lowerer, expr);
if (channel) return channel;
let intlConstructor = expr.expression;
if (ts.isIdentifier(intlConstructor)) {
@@ -8645,20 +8895,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
};
const pattern = strArg(args[0], "pattern");
const flags = strArg(args[1], "flags argument");
- // Node accepts the 'd' and 'v' flags the engine does not have; a
- // literal flags string refuses here like the literal regex form.
- if (flags.kind === "strLit" && args[1] !== undefined) {
- for (const f of ["d", "v"]) {
- if (flags.value.includes(f)) {
- lowerer.unsupported(
- "SC1120",
- args[1],
- f === "d"
- ? "the regex 'd' flag (match indices)"
- : "the regex 'v' flag (unicode sets)",
- );
- }
- }
+ // Node accepts the 'd' flag the engine does not have; a literal
+ // flags string refuses here like the literal regex form.
+ if (flags.kind === "strLit" && args[1] !== undefined && flags.value.includes("d")) {
+ lowerer.unsupported("SC1120", args[1], "the regex 'd' flag (match indices)");
}
return {
kind: "libCall",
@@ -8727,24 +8967,41 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
);
}
// `new StringDecoder(encoding?)` (node:string_decoder): the decoder
- // is a two-field record — the CANONICAL encoding name (aliases fold
- // at compile time, exactly what `.encoding` answers in Node) and
- // the packed-f64 pending state starting at 0 (nothing buffered).
- // The encoding must be a literal (Node's alias set); omitted means
- // utf8, Node's default.
+ // is a two-field record — the CANONICAL encoding name (exactly what
+ // `.encoding` answers in Node) and the packed-f64 pending state
+ // starting at 0 (nothing buffered). A literal Buffer encoding folds
+ // its alias at compile time; any other encoding argument is
+ // normalized at run time (Node's normalizeEncoding, which throws
+ // ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8,
+ // Node's default.
if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) {
const args = expr.arguments ?? [];
if (args.length > 1) {
lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol);
}
- const encName =
- args.length === 1 ? bufEncoding(lowerer, "new StringDecoder", args[0]!) : "utf8";
+ const encNode = args[0];
+ const encT = encNode ? lowerer.typeOf(encNode) : undefined;
+ const encName = !encNode
+ ? "utf8"
+ : encT!.isStringLiteralType()
+ ? knownBufEncoding(encT!.value)
+ : undefined;
+ const encoding: IrExpr =
+ encName !== undefined
+ ? { kind: "strLit", value: encName, type: STRING, loc }
+ : {
+ kind: "libCall",
+ fn: "strdec.encoding",
+ args: [lowerer.lowerExprExpecting(encNode!, DYN)],
+ type: STRING,
+ loc,
+ };
const decT = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (decT?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr));
return {
kind: "recordLit",
fields: [
- { name: "%enc", value: { kind: "strLit", value: encName, type: STRING, loc } },
+ { name: "%enc", value: encoding },
{ name: "%pending", value: { kind: "numLit", value: 0, type: F64, loc } },
],
type: decT,
diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
index c7fae7607..e73bbd4ce 100644
--- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
+++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
@@ -1072,32 +1072,19 @@ export function lowerEmitterMethodCall(
lowerer.checker.getConstructSignatures(lowerer.typeOf(access.expression)).length > 0
) {
if (member === "addAbortListener") return lowerAddAbortListener(lowerer, call, loc);
- if (member === "setMaxListeners" && args.length === 1) {
- if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind !== "f64") {
- // Node's runtime ladder over the dyn value ("setMaxListeners" is
- // the message's slot for the static form).
- const raw = lowerer.lowerExpr(args[0]!);
- if (raw.type.kind === "dyn" || raw.kind === "unitLit" || lowerer.dynConvertible(raw.type)) {
- const n: IrExpr =
- raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc };
- return {
- kind: "libCall",
- fn: "emitter.setDefaultMaxChk",
- args: [n, { kind: "strLit", value: "setMaxListeners", type: STRING, loc }],
- type: VOID,
- loc,
- };
- }
- }
- const n = lowerer.lowerExprExpecting(args[0]!, F64);
- if (n.type.kind !== "f64") {
- lowerer.noLowering(
- `EventEmitter.setMaxListeners with a '${lowerer.fmt(n.type)}' argument`,
- args[0]!,
- "the lowered form takes a number (Node throws ERR_INVALID_ARG_TYPE at runtime for other values)",
- );
- }
- return { kind: "libCall", fn: "emitter.setDefaultMax", args: [n], type: VOID, loc };
+ // The events module's functions are EventEmitter's statics too.
+ if (member === "once") return lowerEventsOnce(lowerer, call, loc);
+ if (member === "setMaxListeners" && args.length === 1)
+ return lowerDefaultMaxListenersSet(lowerer, args[0]!, loc);
+ if (
+ member === "listenerCount" ||
+ member === "getMaxListeners" ||
+ member === "getEventListeners" ||
+ (member === "setMaxListeners" &&
+ args.length > 1 &&
+ args.slice(1).every((arg) => lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "object"))
+ ) {
+ return lowerEventsEmitterHelper(lowerer, call, member, loc)!;
}
if (member === "setMaxListeners") {
// The per-target form with a target that is provably NOT an
@@ -2257,3 +2244,165 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc:
loc,
};
}
+
+/** The EventEmitter argument of an events-module helper: an emitter
+ * instance (not a stream, whose listener tuples the runtime pins). */
+function helperEmitter(lowerer: Lowerer, member: string, node: ts.Expression): IrExpr {
+ const value = lowerer.lowerExpr(node);
+ const info = value.type.kind === "object" ? lowerer.classes.get(value.type.className) : undefined;
+ if (!emitterRooted(lowerer, info) || streamSidesOf(lowerer, info) !== null) {
+ lowerer.noLowering(
+ `events.${member} over this value`,
+ node,
+ "the argument must be an EventEmitter instance; EventTarget and stream arguments are not supported yet",
+ );
+ }
+ return value;
+}
+
+/** setMaxListeners(n) with no targets: the process-wide default, through
+ * Node's validateNumber(n, 'setMaxListeners', 0) ladder. */
+function lowerDefaultMaxListenersSet(lowerer: Lowerer, node: ts.Expression, loc: SrcLoc): IrExpr {
+ if (lowerer.mapTypeOf(lowerer.typeOf(node))?.kind !== "f64") {
+ // Node's runtime ladder over the dyn value ("setMaxListeners" is
+ // the message's slot for the static form).
+ const raw = lowerer.lowerExpr(node);
+ if (raw.type.kind === "dyn" || raw.kind === "unitLit" || lowerer.dynConvertible(raw.type)) {
+ const n: IrExpr =
+ raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc };
+ return {
+ kind: "libCall",
+ fn: "emitter.setDefaultMaxChk",
+ args: [n, { kind: "strLit", value: "setMaxListeners", type: STRING, loc }],
+ type: VOID,
+ loc,
+ };
+ }
+ }
+ const n = lowerer.lowerExprExpecting(node, F64);
+ if (n.type.kind !== "f64") {
+ lowerer.noLowering(
+ `EventEmitter.setMaxListeners with a '${lowerer.fmt(n.type)}' argument`,
+ node,
+ "the lowered form takes a number (Node throws ERR_INVALID_ARG_TYPE at runtime for other values)",
+ );
+ }
+ return { kind: "libCall", fn: "emitter.setDefaultMax", args: [n], type: VOID, loc };
+}
+
+/** The events module's helpers over EventEmitter instances, also reachable
+ * as EventEmitter statics: listenerCount(emitter, name), getMaxListeners
+ * (emitter), setMaxListeners(n[, ...emitters]) and getEventListeners
+ * (emitter, name). Node answers each through the emitter's own method, so
+ * they lower as those methods do. EventTarget and stream arguments are
+ * refused. Null for other members. */
+export function lowerEventsEmitterHelper(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ member: string,
+ loc: SrcLoc,
+): IrExpr | null {
+ const args = call.arguments;
+ if (args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(`events.${member} with spread arguments`, call);
+ }
+ if (member === "listenerCount") {
+ if (args.length !== 2)
+ lowerer.noLowering(`events.listenerCount with ${args.length} arguments`, call);
+ const receiver = helperEmitter(lowerer, member, args[0]!);
+ const name = isErrorMonitorRef(lowerer, args[1]!)
+ ? eventNameExpr(ERROR_MONITOR_EVENT, loc)
+ : lowerer.lowerExprExpecting(args[1]!, STRING);
+ if (name.type.kind !== "string") {
+ lowerer.noLowering(
+ `events.listenerCount with a '${lowerer.fmt(name.type)}' event name`,
+ args[1]!,
+ );
+ }
+ return { kind: "libCall", fn: "emitter.count", args: [receiver, name], type: F64, loc };
+ }
+ if (member === "getMaxListeners") {
+ if (args.length !== 1)
+ lowerer.noLowering(`events.getMaxListeners with ${args.length} arguments`, call);
+ const receiver = helperEmitter(lowerer, member, args[0]!);
+ return { kind: "libCall", fn: "emitter.getMax", args: [receiver], type: F64, loc };
+ }
+ if (member === "setMaxListeners") {
+ // An omitted n is the current default, assigned again.
+ if (args.length === 0) {
+ return {
+ kind: "libCall",
+ fn: "emitter.setDefaultMax",
+ args: [{ kind: "libCall", fn: "emitter.getDefaultMax", args: [], type: F64, loc }],
+ type: VOID,
+ loc,
+ };
+ }
+ if (args.length === 1) return lowerDefaultMaxListenersSet(lowerer, args[0]!, loc);
+ // Node evaluates every argument, validates n, then sets each target.
+ const n = lowerer.lowerExprExpecting(args[0]!, F64);
+ if (n.type.kind !== "f64") {
+ lowerer.noLowering(
+ `events.setMaxListeners with a '${lowerer.fmt(n.type)}' limit and targets`,
+ args[0]!,
+ "pass a number",
+ );
+ }
+ const limit = lowerer.declareHiddenLocal("%maxListeners", F64);
+ const stmts: IrStmt[] = [{ kind: "varDecl", localId: limit.id, init: n, loc }];
+ const targets = args.slice(1).map((node) => {
+ const value = helperEmitter(lowerer, member, node);
+ const slot = lowerer.declareHiddenLocal("%maxListenersTarget", value.type);
+ stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc });
+ return varRef(slot.id, value.type, loc);
+ });
+ for (const target of targets) {
+ stmts.push({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "emitter.setMax",
+ args: [target, varRef(limit.id, F64, loc)],
+ type: target.type,
+ loc,
+ },
+ loc,
+ });
+ }
+ // The effects sit in the statements; the call answers undefined.
+ const tail: IrExpr = { kind: "libCall", fn: "console.noop", args: [], type: VOID, loc };
+ return { kind: "seqExpr", stmts, result: tail, type: VOID, loc };
+ }
+ if (member === "getEventListeners") {
+ if (args.length !== 2)
+ lowerer.noLowering(`events.getEventListeners with ${args.length} arguments`, call);
+ // emitter.listeners(name): the event's listener array, typed by the
+ // event's unified tuple (the instance method's rules).
+ const receiver = helperEmitter(lowerer, member, args[0]!);
+ const name = eventNameOf(lowerer, "events.getEventListeners", args[1]!);
+ const table = emitterEvents(lowerer);
+ const sig = table.get(name);
+ if (
+ sig?.dynListener ||
+ flexibleJsEvent(name, sig) ||
+ computedPatterns(lowerer).some(
+ (pattern) => pattern.listener && patternMatchesName(pattern, name),
+ )
+ ) {
+ lowerer.noLowering(
+ `events.getEventListeners of the event '${name}'`,
+ call,
+ "a listener of this event uses checked-dynamic dispatch, so the listener array has no one static element type — listenerCount(emitter, name) counts",
+ );
+ }
+ const tuple = tupleOf(lowerer, table, name, call);
+ return {
+ kind: "libCall",
+ fn: "emitter.listeners",
+ args: [receiver, eventNameExpr(name, loc)],
+ type: arrayOf({ kind: "func", params: tuple, ret: { kind: "void" } }),
+ loc,
+ };
+ }
+ return null;
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index af9e64f30..f0538bb71 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -1,7 +1,9 @@
+import { dateJsonType, isDateJsonSource } from "./builtins/dates.js";
import { guardNullProto, nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js";
import { lowerProtocolPropertyRead } from "./class-protocol-dispatch.js";
import { generatorDrain } from "./iterator-adapters.js";
import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower-event-target.js";
+import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js";
import { lowerEventStaticProperty } from "./builtins/numbers.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
@@ -9,6 +11,7 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
import { lowerReadlineProperty } from "./builtins/readline.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
+import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
import { threadsImportOf } from "../threads-import.js";
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
@@ -224,7 +227,7 @@ import {
} from "./lower-builtin-values.js";
import { lowerArrayFromValue } from "./containers/array-construction.js";
import { lowerPerfHooksTypeof } from "./builtins/performance.js";
-import { lowerUrlAssignment } from "./builtins/url.js";
+import { isSearchParamsIterable, lowerUrlAssignment } from "./builtins/url.js";
import { jsBindingHasOpenWrites } from "./lower-stmts.js";
import { checkedClassInstanceOf } from "./class-construction.js";
import { lowerModuleNamespaceElement } from "./module-namespace-elements.js";
@@ -645,6 +648,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// have non-stdlib symbols and keep the ordinary path.
let tested = expr.expression;
while (ts.isParenthesizedExpression(tested)) tested = tested.expression;
+ // `typeof zlib.Inflate` / `typeof zlib.createGzip`: an export of a
+ // Node builtin module read through a plain binding (an import or
+ // require result). The read has no side effects, and Node's answer for
+ // every exported function and class is "function".
+ if (
+ ts.isPropertyAccessExpression(tested) &&
+ ts.isIdentifier(tested.expression) &&
+ lowerer.isStdlibMember(tested)
+ ) {
+ const sym = lowerer.checker.getSymbolAtLocation(tested.name);
+ const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
+ if (
+ decls.some(isBuiltinModuleValueDecl) &&
+ decls.every((decl) => builtinModuleExportParent(decl) !== undefined)
+ ) {
+ const t = lowerer.typeOf(tested);
+ if (
+ lowerer.checker.getCallSignatures(t).length > 0 ||
+ lowerer.checker.getConstructSignatures(t).length > 0
+ ) {
+ return { kind: "strLit", value: "function", type: STRING, loc };
+ }
+ }
+ }
if (ts.isIdentifier(tested)) {
const sym = lowerer.checker.getSymbolAtLocation(tested);
if (!sym) {
@@ -3310,7 +3337,11 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
(expr.kind === "recordGet" || expr.kind === "recordKeyGet") &&
(ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))
) {
- const receiver = lowerer.mapTypeOf(lowerer.typeOf(node.expression));
+ // The read itself rules out a null or undefined receiver, which a
+ // JavaScript receiver such as `m.groups` still declares.
+ const receiver = lowerer.mapTypeOf(
+ lowerer.checker.getNonNullableType(lowerer.typeOf(node.expression)),
+ );
const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined;
const declared =
expr.kind === "recordGet"
@@ -3322,10 +3353,23 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
!(declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
)
return expr;
+ // The same for a field holding a Date's toJSON() result, whose null
+ // the declared string does not represent.
+ if (
+ expr.kind === "recordGet" &&
+ lowerer.isRuntimeOptionalField(expr.shapeId, expr.field) &&
+ lowerer.armTag(expr.type.unionId, NULL_T) >= 0 &&
+ declared &&
+ !(declared.kind === "union" && lowerer.armTag(declared.unionId, NULL_T) >= 0)
+ )
+ return expr;
}
if (ts.isIdentifier(node)) {
const symbol = lowerer.resolveValueSymbol(node);
if (symbol && lowerer.unstableSwitchBindings.has(symbol)) return expr;
+ // A binding initialized by a Date's toJSON() can hold null where the
+ // checker says string; consumers check it.
+ if (symbol && lowerer.dateJsonBindings.has(symbol)) return expr;
}
if (
ts.isPropertyAccessExpression(node) &&
@@ -4927,6 +4971,8 @@ export function lowerIntrinsicProperty(
return lowerNumberParserValue(lowerer, numberMember, locOf(expr));
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "poolSize")
return { kind: "libCall", fn: "buffer.poolSize", args: [], type: F64, loc: locOf(expr) };
+ const netAddressStatic = lowerNetAddressStaticRead(lowerer, expr);
+ if (netAddressStatic) return netAddressStatic;
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer")
return lowerCheckedPredicateValue(lowerer, "Buffer.isBuffer", "buffer", locOf(expr));
if (lowerer.stdlibGlobalMember(expr, "Date") === "now")
@@ -5310,6 +5356,27 @@ export function lowerIntrinsicProperty(
loc: locOf(expr),
};
}
+ // The flag getters answer whether `flags` holds the flag's letter.
+ const flag = REGEX_FLAG_GETTERS.get(name);
+ if (flag !== undefined) {
+ const loc = locOf(expr);
+ const flags: IrExpr = {
+ kind: "regexIntrinsic",
+ method: "flags",
+ receiver: strictReceiver({ kind: "regex" }),
+ args: [],
+ type: STRING,
+ loc,
+ };
+ return {
+ kind: "strIntrinsic",
+ method: "includes",
+ receiver: flags,
+ args: [{ kind: "strLit", value: flag, type: STRING, loc }],
+ type: BOOL,
+ loc,
+ };
+ }
if (name === "test") {
lowerer.unsupported("SC1090", expr, "regex methods as values (call 'test' directly)");
}
@@ -5494,6 +5561,31 @@ export function lowerIntrinsicProperty(
return null;
}
+/** JavaScript: whether `expr` is or holds an existing checked-dynamic
+ * object — a binding stored as a dyn value whose checker type is an
+ * object, or an array or object literal with such an element. Building
+ * the enclosing literal with a static element type would copy it. */
+export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean {
+ while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
+ if (ts.isIdentifier(expr)) {
+ const stored = lowerer.peekLocal(expr)?.type ?? lowerer.globalOf(expr)?.type;
+ return stored?.kind === "dyn" && (lowerer.typeOf(expr).flags & ts.TypeFlags.Object) !== 0;
+ }
+ if (ts.isArrayLiteralExpression(expr))
+ return expr.elements.some(
+ (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element),
+ );
+ if (ts.isObjectLiteralExpression(expr))
+ return expr.properties.some(
+ (property) =>
+ (ts.isPropertyAssignment(property) && jsHoldsDynObject(lowerer, property.initializer)) ||
+ (ts.isShorthandPropertyAssignment(property) &&
+ ts.isIdentifier(property.name) &&
+ jsHoldsDynObject(lowerer, property.name)),
+ );
+ return false;
+}
+
/** `[a, b, c]`. The element type comes from the contextual type when tsc
* has one (`const a: number[] = []`, arguments, nested literals) and from
* the literal's own inferred type otherwise. A bare `[]` with no context
@@ -5621,6 +5713,15 @@ export function lowerArrayLiteral(
}
}
}
+ // A Date's toJSON() element keeps its null in a literal with no declared
+ // destination: the lib types say string.
+ if (
+ !expected &&
+ mapped?.kind === "array" &&
+ mapped.elem.kind === "string" &&
+ expr.elements.some((element) => isDateJsonSource(lowerer, element))
+ )
+ mapped = { kind: "array", elem: dateJsonType(lowerer) };
// An array literal inside an npm-static implicit-any instance can
// inherit the checker's poisoned any[] context even after the
// active binding gives every element one concrete lowering. Recover a
@@ -5665,6 +5766,18 @@ export function lowerArrayLiteral(
)
)
mapped = DYN;
+ // An element that is an existing checked-dynamic object (a file-scope
+ // object literal binding, or a literal holding one) keeps its identity
+ // only in a checked-dynamic array: a static element type would copy it.
+ if (
+ !expected &&
+ isJsSourceFile(expr.getSourceFile()) &&
+ mapped?.kind !== "dyn" &&
+ expr.elements.some(
+ (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element),
+ )
+ )
+ mapped = DYN;
// An EMPTY literal under a CONST ASSERTION whose type queries panicked
// (tsgo's readonly-[] TupleType conversion) uses a unit-element array,
// mapType's own `[] as const` rule.
@@ -5849,6 +5962,11 @@ export function lowerArrayLiteral(
const drained = generatorDrain(lowerer, src, type.elem, locOf(el));
if (drained) src = drained;
}
+ // `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its
+ // checked-dynamic view iterates the live pair list, and the drained
+ // [name, value] pairs convert to the literal's tuple elements.
+ const searchParamsSource = isSearchParamsIterable(lowerer, el.expression);
+ if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN);
// A native checked iterable can supply scalar elements even when
// the checker inferred a typed array from its producer. Drain once
// and validate the elements before the ordinary spread copy.
@@ -5857,7 +5975,8 @@ export function lowerArrayLiteral(
(type.elem.kind === "dyn" ||
type.elem.kind === "f64" ||
type.elem.kind === "string" ||
- type.elem.kind === "bool")
+ type.elem.kind === "bool" ||
+ searchParamsSource)
) {
src = lowerer.coerceInto(el.expression, checkedIteratorPack(lowerer, src, locOf(el)), type);
}
@@ -10227,7 +10346,6 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
lowerOptionalNumber(lowerer, operand, loc, node);
const arithmeticLeft = strictEquality ? left : arithmeticNumber(left, expr.left);
const arithmeticRight = strictEquality ? right : arithmeticNumber(right, expr.right);
- const plainBothNum = left.type.kind === "f64" && right.type.kind === "f64";
const bothNum = arithmeticLeft.type.kind === "f64" && arithmeticRight.type.kind === "f64";
const bothStr = left.type.kind === "string" && right.type.kind === "string";
@@ -10449,234 +10567,17 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
};
}
case ts.SyntaxKind.EqualsEqualsEqualsToken:
- case ts.SyntaxKind.ExclamationEqualsEqualsToken: {
- const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
- const indexed = tryLowerIndexedComparison(lowerer, left, right, negated, loc);
- if (indexed) return indexed;
- if (plainBothNum)
- return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
- if (bothStr) {
- if (left.kind === "strLit" && right.kind === "strLit")
- return {
- kind: "boolLit",
- value: (left.value === right.value) !== negated,
- type: BOOL,
- loc,
- };
- return { kind: "strEq", negated, left, right, type: BOOL, loc };
- }
- // bool === bool: a plain value compare (the config-drift checks'
- // `desired.lanMode !== actual.lanMode` shape).
- if (left.type.kind === "bool" && right.type.kind === "bool") {
- return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
- }
- const unitTest = lowerer.lowerUnitComparison(left, right, negated, loc);
- if (unitTest) return unitTest;
- if (left.type.kind === "union" || right.type.kind === "union") {
- const compared = lowerUnionEquality(lowerer, left, right, negated, false, loc);
- if (compared) return compared;
- const aliasRefusal = (type: IrType): void => {
- if (tagEqualityMayMissAlias(lowerer, type))
- lowerer.unsupported(
- "SC1090",
- expr,
- `comparisons of related class values beside structural union members (${NARROW_FIRST})`,
- );
- };
-
- // JS strict equality of the ARM values, per union tag (a
- // per-union helper): equal tags compare payloads (f64 ==, string
- // bytes, bool ==, ref-arm POINTER identity), different tags are
- // never equal. Two shapes lower: same union on both sides, and
- // union vs plain arm value (`u === "text"` where the checker
- // didn't narrow) — the plain side wraps into the union, which
- // preserves payload identity. Different unions (tsc admits
- // partially-overlapping ones) still need narrowing first.
- const ut =
- left.type.kind === "union" ? left.type : (right.type as IrType & { kind: "union" });
- const bothUnion = left.type.kind === "union" && right.type.kind === "union";
- const sameUnion = bothUnion && typeEquals(left.type, right.type);
- const plainFunc =
- left.type.kind === "func" ? left : right.type.kind === "func" ? right : null;
- const unionArms = lowerer.unions.get(ut.unionId)?.arms ?? [];
- const funcArm = unionArms.find((arm) => arm.kind === "func");
- // `listeners()` reports original prefix callbacks, but the
- // checker views an indexed result as function | undefined. For
- // identity comparison, retain the original closure pointer rather
- // than adapting it to the tuple signature.
- if (
- !bothUnion &&
- plainFunc !== null &&
- funcArm?.kind === "func" &&
- unionArms.filter((arm) => arm.kind === "func").length === 1 &&
- unionArms.some(isUnitType)
- ) {
- return {
- kind: "unionFuncEq",
- unionId: ut.unionId,
- tag: lowerer.armTag(ut.unionId, funcArm),
- union: left.type.kind === "union" ? left : right,
- func: plainFunc,
- negated,
- type: BOOL,
- loc,
- };
- }
- if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) {
- aliasRefusal(ut);
- return {
- kind: "unionEq",
- unionId: ut.unionId,
- negated,
- sameValue: false,
- left: lowerer.coerceInto(expr.left, left, ut),
- right: lowerer.coerceInto(expr.right, right, ut),
- type: BOOL,
- loc,
- };
- }
- if (left.type.kind === "union" && right.type.kind === "union") {
- const leftDef = lowerer.unions.get(left.type.unionId);
- const rightDef = lowerer.unions.get(right.type.unionId);
- if (leftDef && rightDef) {
- const leftValues = leftDef.arms.filter((arm) => !isUnitType(arm));
- const rightValues = rightDef.arms.filter((arm) => !isUnitType(arm));
- const sameValueArms =
- leftValues.length === rightValues.length &&
- leftValues.every((arm) =>
- rightValues.some((candidate) => typeEquals(candidate, arm)),
- );
- if (sameValueArms) {
- const arms = [...leftDef.arms];
- for (const arm of rightDef.arms) {
- if (!arms.some((candidate) => typeEquals(candidate, arm))) arms.push(arm);
- }
- arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
- const common: IrType & { kind: "union" } = {
- kind: "union",
- unionId: lowerer.unions.intern(arms),
- };
- aliasRefusal(common);
- return {
- kind: "unionEq",
- unionId: common.unionId,
- negated,
- sameValue: false,
- left: lowerer.coerceInto(expr.left, left, common),
- right: lowerer.coerceInto(expr.right, right, common),
- type: BOOL,
- loc,
- };
- }
- }
- }
- lowerer.unsupported("SC1090", expr, `comparisons of union-typed values (${NARROW_FIRST})`);
- }
- // Arrays, maps, functions, class instances and records compare by
- // reference identity (pointer compare) — JS-exact object equality.
- // Hierarchy-related classes compare after widening the derived side
- // (same pointer either way — prefix layout).
- let idLeft = left;
- let idRight = right;
- if (left.type.kind === "object" && right.type.kind === "object") {
- if (lowerer.isSubclassOf(left.type.className, right.type.className)) {
- idLeft = lowerer.upcastTo(left, right.type.className);
- } else if (lowerer.isSubclassOf(right.type.className, left.type.className)) {
- idRight = lowerer.upcastTo(right, left.type.className);
- }
- }
- // Two function values compare by identity whatever their STATIC
- // signatures (tsc admits the comparison when one side is
- // assignable to the other — a tuple-typed listeners() element
- // against a prefix-declared handler): the pointer answers it.
- if (idLeft.type.kind === "func" && idRight.type.kind === "func") {
- return {
- kind: "bin",
- op: negated ? "!==" : "===",
- left: idLeft,
- right: idRight,
- type: BOOL,
- loc,
- };
- }
- // Two CLASS values compare by identity whatever their static
- // classes — one immortal object per class, one pointer compare
- // (the function-identity rule verbatim; `X === D` through a
- // base-typed slot answers exactly JS).
- if (idLeft.type.kind === "classval" && idRight.type.kind === "classval") {
- return {
- kind: "bin",
- op: negated ? "!==" : "===",
- left: idLeft,
- right: idRight,
- type: BOOL,
- loc,
- };
- }
- if (idLeft.type.kind === "moduleNs" && idRight.type.kind === "moduleNs") {
- return {
- kind: "bin",
- op: negated ? "!==" : "===",
- left: idLeft,
- right: idRight,
- type: BOOL,
- loc,
- };
- }
- if (
- (idLeft.type.kind === "array" ||
- idLeft.type.kind === "map" ||
- idLeft.type.kind === "set" ||
- idLeft.type.kind === "regex" ||
- idLeft.type.kind === "url" ||
- idLeft.type.kind === "object" ||
- idLeft.type.kind === "record" ||
- // Symbols ARE identity: `Symbol('a') === Symbol('a')` is false,
- // a symbol equals exactly itself, and Symbol.for's interned
- // values compare equal across call sites — all one pointer
- // compare, JS's spec without approximation.
- idLeft.type.kind === "symbol" ||
- // Typed arrays / Buffers ARE objects to ===: pointer identity
- // (buf === buf.swap16() — the in-place mutators return this).
- idLeft.type.kind === "bytes" ||
- idLeft.type.kind === "promise") &&
- typeEquals(idLeft.type, idRight.type)
- ) {
- return {
- kind: "bin",
- op: negated ? "!==" : "===",
- left: idLeft,
- right: idRight,
- type: BOOL,
- loc,
- };
- }
- // Runtime HANDLES are objects to === too: one handle per socket/
- // request/response, so pointer identity IS JS's object equality
- // (`c.pause() === c` — Node's chaining assertions). */
- if (DYN_HANDLE_KINDS.has(idLeft.type.kind) && typeEquals(idLeft.type, idRight.type)) {
- return {
- kind: "bin",
- op: negated ? "!==" : "===",
- left: idLeft,
- right: idRight,
- type: BOOL,
- loc,
- };
- }
- if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) {
- return {
- kind: "dynScalarEq",
- left: lowerer.coerceToExpected(idLeft, DYN),
- right: lowerer.coerceToExpected(idRight, DYN),
- ...(negated ? { negated: true as const } : {}),
- type: BOOL,
- loc,
- };
- }
- lowerer.unsupported("SC1043", expr);
- break;
- }
+ case ts.SyntaxKind.ExclamationEqualsEqualsToken:
+ return lowerStrictEquality(
+ lowerer,
+ expr,
+ expr.left,
+ expr.right,
+ left,
+ right,
+ op === ts.SyntaxKind.ExclamationEqualsEqualsToken,
+ loc,
+ );
case ts.SyntaxKind.LessThanToken:
case ts.SyntaxKind.LessThanEqualsToken:
case ts.SyntaxKind.GreaterThanToken:
@@ -10699,7 +10600,14 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc };
- if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) {
+ // A Date converts to a new checked-dynamic Date on each crossing, so
+ // comparisons keep their Date fence instead of comparing copies.
+ if (
+ left.type.kind !== "date" &&
+ right.type.kind !== "date" &&
+ lowerer.dynConvertible(left.type) &&
+ lowerer.dynConvertible(right.type)
+ ) {
return {
kind: "libCall",
fn: "dyn.compare",
@@ -10725,6 +10633,250 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
);
}
+/** `left === right` (`!==` when negated) over lowered operands: JS strict
+ * equality for every pair of static kinds the program can compare.
+ * `node` anchors diagnostics, and `leftNode`/`rightNode` the coercions. */
+export function lowerStrictEquality(
+ lowerer: Lowerer,
+ node: ts.Node,
+ leftNode: ts.Node,
+ rightNode: ts.Node,
+ left: IrExpr,
+ right: IrExpr,
+ negated: boolean,
+ loc: SrcLoc,
+): IrExpr {
+ const plainBothNum = left.type.kind === "f64" && right.type.kind === "f64";
+ const bothStr = left.type.kind === "string" && right.type.kind === "string";
+ const indexed = tryLowerIndexedComparison(lowerer, left, right, negated, loc);
+ if (indexed) return indexed;
+ if (plainBothNum)
+ return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
+ if (bothStr) {
+ if (left.kind === "strLit" && right.kind === "strLit")
+ return {
+ kind: "boolLit",
+ value: (left.value === right.value) !== negated,
+ type: BOOL,
+ loc,
+ };
+ return { kind: "strEq", negated, left, right, type: BOOL, loc };
+ }
+ // bool === bool: a plain value compare (the config-drift checks'
+ // `desired.lanMode !== actual.lanMode` shape).
+ if (left.type.kind === "bool" && right.type.kind === "bool") {
+ return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
+ }
+ const unitTest = lowerer.lowerUnitComparison(left, right, negated, loc);
+ if (unitTest) return unitTest;
+ if (left.type.kind === "union" || right.type.kind === "union") {
+ const compared = lowerUnionEquality(lowerer, left, right, negated, false, loc);
+ if (compared) return compared;
+ const aliasRefusal = (type: IrType): void => {
+ if (tagEqualityMayMissAlias(lowerer, type))
+ lowerer.unsupported(
+ "SC1090",
+ node,
+ `comparisons of related class values beside structural union members (${NARROW_FIRST})`,
+ );
+ };
+
+ // JS strict equality of the ARM values, per union tag (a
+ // per-union helper): equal tags compare payloads (f64 ==, string
+ // bytes, bool ==, ref-arm POINTER identity), different tags are
+ // never equal. Two shapes lower: same union on both sides, and
+ // union vs plain arm value (`u === "text"` where the checker
+ // didn't narrow) — the plain side wraps into the union, which
+ // preserves payload identity. Different unions (tsc admits
+ // partially-overlapping ones) still need narrowing first.
+ const ut = left.type.kind === "union" ? left.type : (right.type as IrType & { kind: "union" });
+ const bothUnion = left.type.kind === "union" && right.type.kind === "union";
+ const sameUnion = bothUnion && typeEquals(left.type, right.type);
+ const plainFunc = left.type.kind === "func" ? left : right.type.kind === "func" ? right : null;
+ const unionArms = lowerer.unions.get(ut.unionId)?.arms ?? [];
+ const funcArm = unionArms.find((arm) => arm.kind === "func");
+ // `listeners()` reports original prefix callbacks, but the
+ // checker views an indexed result as function | undefined. For
+ // identity comparison, retain the original closure pointer rather
+ // than adapting it to the tuple signature.
+ if (
+ !bothUnion &&
+ plainFunc !== null &&
+ funcArm?.kind === "func" &&
+ unionArms.filter((arm) => arm.kind === "func").length === 1 &&
+ unionArms.some(isUnitType)
+ ) {
+ return {
+ kind: "unionFuncEq",
+ unionId: ut.unionId,
+ tag: lowerer.armTag(ut.unionId, funcArm),
+ union: left.type.kind === "union" ? left : right,
+ func: plainFunc,
+ negated,
+ type: BOOL,
+ loc,
+ };
+ }
+ if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) {
+ aliasRefusal(ut);
+ return {
+ kind: "unionEq",
+ unionId: ut.unionId,
+ negated,
+ sameValue: false,
+ left: lowerer.coerceInto(leftNode, left, ut),
+ right: lowerer.coerceInto(rightNode, right, ut),
+ type: BOOL,
+ loc,
+ };
+ }
+ if (left.type.kind === "union" && right.type.kind === "union") {
+ const leftDef = lowerer.unions.get(left.type.unionId);
+ const rightDef = lowerer.unions.get(right.type.unionId);
+ if (leftDef && rightDef) {
+ const leftValues = leftDef.arms.filter((arm) => !isUnitType(arm));
+ const rightValues = rightDef.arms.filter((arm) => !isUnitType(arm));
+ const sameValueArms =
+ leftValues.length === rightValues.length &&
+ leftValues.every((arm) => rightValues.some((candidate) => typeEquals(candidate, arm)));
+ if (sameValueArms) {
+ const arms = [...leftDef.arms];
+ for (const arm of rightDef.arms) {
+ if (!arms.some((candidate) => typeEquals(candidate, arm))) arms.push(arm);
+ }
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
+ const common: IrType & { kind: "union" } = {
+ kind: "union",
+ unionId: lowerer.unions.intern(arms),
+ };
+ aliasRefusal(common);
+ return {
+ kind: "unionEq",
+ unionId: common.unionId,
+ negated,
+ sameValue: false,
+ left: lowerer.coerceInto(leftNode, left, common),
+ right: lowerer.coerceInto(rightNode, right, common),
+ type: BOOL,
+ loc,
+ };
+ }
+ }
+ }
+ lowerer.unsupported("SC1090", node, `comparisons of union-typed values (${NARROW_FIRST})`);
+ }
+ // Arrays, maps, functions, class instances and records compare by
+ // reference identity (pointer compare) — JS-exact object equality.
+ // Hierarchy-related classes compare after widening the derived side
+ // (same pointer either way — prefix layout).
+ let idLeft = left;
+ let idRight = right;
+ if (left.type.kind === "object" && right.type.kind === "object") {
+ if (lowerer.isSubclassOf(left.type.className, right.type.className)) {
+ idLeft = lowerer.upcastTo(left, right.type.className);
+ } else if (lowerer.isSubclassOf(right.type.className, left.type.className)) {
+ idRight = lowerer.upcastTo(right, left.type.className);
+ }
+ }
+ // Two function values compare by identity whatever their STATIC
+ // signatures (tsc admits the comparison when one side is
+ // assignable to the other — a tuple-typed listeners() element
+ // against a prefix-declared handler): the pointer answers it.
+ if (idLeft.type.kind === "func" && idRight.type.kind === "func") {
+ return {
+ kind: "bin",
+ op: negated ? "!==" : "===",
+ left: idLeft,
+ right: idRight,
+ type: BOOL,
+ loc,
+ };
+ }
+ // Two CLASS values compare by identity whatever their static
+ // classes — one immortal object per class, one pointer compare
+ // (the function-identity rule verbatim; `X === D` through a
+ // base-typed slot answers exactly JS).
+ if (idLeft.type.kind === "classval" && idRight.type.kind === "classval") {
+ return {
+ kind: "bin",
+ op: negated ? "!==" : "===",
+ left: idLeft,
+ right: idRight,
+ type: BOOL,
+ loc,
+ };
+ }
+ if (idLeft.type.kind === "moduleNs" && idRight.type.kind === "moduleNs") {
+ return {
+ kind: "bin",
+ op: negated ? "!==" : "===",
+ left: idLeft,
+ right: idRight,
+ type: BOOL,
+ loc,
+ };
+ }
+ if (
+ (idLeft.type.kind === "array" ||
+ idLeft.type.kind === "map" ||
+ idLeft.type.kind === "set" ||
+ idLeft.type.kind === "regex" ||
+ idLeft.type.kind === "url" ||
+ idLeft.type.kind === "object" ||
+ idLeft.type.kind === "record" ||
+ // Symbols ARE identity: `Symbol('a') === Symbol('a')` is false,
+ // a symbol equals exactly itself, and Symbol.for's interned
+ // values compare equal across call sites — all one pointer
+ // compare, JS's spec without approximation.
+ idLeft.type.kind === "symbol" ||
+ // Typed arrays / Buffers ARE objects to ===: pointer identity
+ // (buf === buf.swap16() — the in-place mutators return this).
+ idLeft.type.kind === "bytes" ||
+ idLeft.type.kind === "promise") &&
+ typeEquals(idLeft.type, idRight.type)
+ ) {
+ return {
+ kind: "bin",
+ op: negated ? "!==" : "===",
+ left: idLeft,
+ right: idRight,
+ type: BOOL,
+ loc,
+ };
+ }
+ // Runtime HANDLES are objects to === too: one handle per socket/
+ // request/response, so pointer identity IS JS's object equality
+ // (`c.pause() === c` — Node's chaining assertions). */
+ if (DYN_HANDLE_KINDS.has(idLeft.type.kind) && typeEquals(idLeft.type, idRight.type)) {
+ return {
+ kind: "bin",
+ op: negated ? "!==" : "===",
+ left: idLeft,
+ right: idRight,
+ type: BOOL,
+ loc,
+ };
+ }
+ // A Date converts to a new checked-dynamic Date on each crossing; its
+ // identity has no representation, so equality keeps the fence.
+ if (
+ idLeft.type.kind !== "date" &&
+ idRight.type.kind !== "date" &&
+ lowerer.dynConvertible(idLeft.type) &&
+ lowerer.dynConvertible(idRight.type)
+ ) {
+ return {
+ kind: "dynScalarEq",
+ left: lowerer.coerceToExpected(idLeft, DYN),
+ right: lowerer.coerceToExpected(idRight, DYN),
+ ...(negated ? { negated: true as const } : {}),
+ type: BOOL,
+ loc,
+ };
+ }
+ return lowerer.unsupported("SC1043", node);
+}
+
function lowerLogicalChain(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr {
// Logical chains have a left-nested AST. Keep the native stack bounded
// while retaining each pair's checker type and lazy operand evaluation.
@@ -11913,6 +12065,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.nativeUrlIs", args: [value], type: BOOL, loc };
}
+ {
+ const stack = lowerDisposableStackInstanceOf(lowerer, expr);
+ if (stack) return stack;
+ }
if (lowerer.isStdlibGlobal(expr.right, "DataView")) {
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return { kind: "libCall", fn: "dyn.dataViewIs", args: [value], type: BOOL, loc };
@@ -11932,6 +12088,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
if (blob) return blob;
const keyObject = lowerKeyObjectInstanceof(lowerer, expr);
if (keyObject) return keyObject;
+ const netAddress = lowerNetAddressInstanceof(lowerer, expr);
+ if (netAddress) return netAddress;
}
if (lowerer.isStdlibGlobal(expr.right, "SharedArrayBuffer")) {
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
@@ -13152,10 +13310,22 @@ function lowerRuntimeKeyIn(
return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc };
}
+/** RegExp.prototype's flag getters and the flag each one reads. */
+const REGEX_FLAG_GETTERS: ReadonlyMap = new Map([
+ ["hasIndices", "d"],
+ ["global", "g"],
+ ["ignoreCase", "i"],
+ ["multiline", "m"],
+ ["dotAll", "s"],
+ ["unicode", "u"],
+ ["unicodeSets", "v"],
+ ["sticky", "y"],
+]);
+
/** A regex literal `/ab+c/gi` → regexLit (fresh state per evaluation,
* shared bytecode per (pattern, flags) pair). The TS parser has already syntax-checked the literal;
- * what remains here is the flag-alphabet fence (d and v are
- * declared-valid TS flags outside this slice). Named capture groups
+ * what remains here is the flag-alphabet fence (d is a
+ * declared-valid TS flag outside this slice). Named capture groups
* `(?...)` and `\k` backreferences compile — libregexp
* executes them natively, replace templates resolve `$` at
* runtime, and `.groups` reads desugar at their access sites
@@ -13166,20 +13336,19 @@ export function lowerRegexLiteral(lowerer: Lowerer, expr: ts.RegularExpressionLi
const text = expr.text;
const lastSlash = text.lastIndexOf("/");
const pattern = text.slice(1, lastSlash);
- const flags = text.slice(lastSlash + 1);
- for (const f of flags) {
- if (!"gimsuy".includes(f)) {
+ const written = text.slice(lastSlash + 1);
+ for (const f of written) {
+ if (!"gimsuvy".includes(f)) {
lowerer.unsupported(
"SC1120",
expr,
- f === "d"
- ? "the regex 'd' flag (match indices)"
- : f === "v"
- ? "the regex 'v' flag (unicode sets)"
- : `the regex '${f}' flag`,
+ f === "d" ? "the regex 'd' flag (match indices)" : `the regex '${f}' flag`,
);
}
}
+ // `flags`, `toString()` and inspection list the flags in getter order,
+ // whatever order the literal spells them in.
+ const flags = [..."gimsuvy"].filter((f) => written.includes(f)).join("");
return { kind: "regexLit", pattern, flags, type: REGEX, loc: locOf(expr) };
}
@@ -13386,6 +13555,15 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression
if (ts.isElementAccessExpression(expr)) {
return matchAllRegexOf(lowerer, expr.expression);
}
+ // A row picked by `rows.find(cb)`, `rows.findLast(cb)` or `rows.at(i)`.
+ if (
+ ts.isCallExpression(expr) &&
+ ts.isPropertyAccessExpression(expr.expression) &&
+ MATCH_ROW_PICK_METHODS.has(expr.expression.name.text) &&
+ lowerer.isStdlibMember(expr.expression)
+ ) {
+ return matchAllRegexOf(lowerer, expr.expression.expression);
+ }
if (ts.isIdentifier(expr)) {
// The for-of binding over a matchAll drain: `for (const m of
// s.matchAll(re))` (or over a stored const rows).
@@ -13402,10 +13580,77 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression
// A stored const match result: `const m = re.exec(s)`.
const init = constInitializerOf(lowerer, expr);
if (init !== null) return matchProducerRegexOf(lowerer, init);
+ // The element parameter of an array callback over matchAll rows:
+ // `[...s.matchAll(re)].map((m) => m.groups)`, and Array.from's
+ // mapping function over `s.matchAll(re)`.
+ if (decl !== undefined && ts.isParameter(decl)) return matchAllCallbackRegexOf(lowerer, decl);
}
return null;
}
+/** Array methods that return one row of the receiver. */
+const MATCH_ROW_PICK_METHODS = new Set(["find", "findLast", "at"]);
+
+/** Array methods whose result holds rows of the receiver. */
+const MATCH_ROW_SUBSET_METHODS = new Set(["filter", "slice", "toReversed", "toSorted"]);
+
+const MATCH_ROW_CALLBACK_METHODS = new Set([
+ "map",
+ "forEach",
+ "filter",
+ "find",
+ "findLast",
+ "some",
+ "every",
+ "flatMap",
+]);
+
+/** The regex behind `param` when it is the first parameter of an inline
+ * callback whose rows come from a matchAll drain. */
+function matchAllCallbackRegexOf(
+ lowerer: Lowerer,
+ param: ts.ParameterDeclaration,
+): ts.Expression | null {
+ const fn = param.parent;
+ if (!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.parameters[0] !== param)
+ return null;
+ // A reassigned parameter can hold a row of another regex.
+ if (ts.isIdentifier(param.name)) {
+ const sym = lowerer.checker.getSymbolAtLocation(param.name);
+ if (sym === undefined || !bindingNeverReassigned(lowerer, sym, param)) return null;
+ }
+ let site: ts.Node = fn;
+ while (ts.isParenthesizedExpression(site.parent)) site = site.parent;
+ const call = site.parent;
+ if (!ts.isCallExpression(call) || !ts.isPropertyAccessExpression(call.expression)) return null;
+ const method = call.expression.name.text;
+ if (
+ MATCH_ROW_CALLBACK_METHODS.has(method) &&
+ call.arguments[0] === site &&
+ lowerer.isStdlibMember(call.expression)
+ )
+ return matchAllRegexOf(lowerer, call.expression.expression);
+ if (
+ method === "from" &&
+ call.arguments[1] === site &&
+ lowerer.isStdlibGlobal(call.expression.expression, "Array") &&
+ call.arguments[0] !== undefined
+ )
+ return matchAllRegexOf(lowerer, call.arguments[0]);
+ return null;
+}
+
+/** The named groups of the rows `param` receives when it is the first
+ * parameter of an inline matchAll callback (matchAllCallbackRegexOf). */
+export function matchCallbackParamGroupsOf(
+ lowerer: Lowerer,
+ param: ts.ParameterDeclaration,
+): { name: string; index: number }[] | null {
+ const reExpr = matchAllCallbackRegexOf(lowerer, param);
+ const pattern = reExpr === null ? null : staticRegexPatternOf(lowerer, reExpr);
+ return pattern === null ? null : namedCaptureGroupsOfPattern(pattern);
+}
+
/** The regex argument of a (possibly const-stored) `s.matchAll(re)`. */
function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | null {
let expr = e;
@@ -13431,6 +13676,14 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu
const only = expr.elements[0]!;
if (ts.isSpreadElement(only)) return matchAllRegexOf(lowerer, only.expression);
}
+ if (
+ ts.isCallExpression(expr) &&
+ ts.isPropertyAccessExpression(expr.expression) &&
+ MATCH_ROW_SUBSET_METHODS.has(expr.expression.name.text) &&
+ lowerer.isStdlibMember(expr.expression)
+ ) {
+ return matchAllRegexOf(lowerer, expr.expression.expression);
+ }
if (ts.isIdentifier(expr)) {
const init = constInitializerOf(lowerer, expr);
if (init !== null) return matchAllRegexOf(lowerer, init);
@@ -13454,11 +13707,11 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu
* - ES2025 duplicate names (distinct alternatives) project the first
* participating occurrence, including a participating empty string;
* - key order is group declaration order (declaredOrder), Node's own.
- * The record has no prototype — Node's groups object is null-prototype,
- * observable only through util.inspect's "[Object: null prototype]"
- * prefix (the Object.groupBy stance, ledgered). Null when the receiver
- * isn't a match slice or the regex isn't traceable — the caller's
- * member fence (with the groups hint) names the gap. */
+ * The record has a null prototype, like Node's groups object. A
+ * JavaScript callback parameter holds the row as a dynamic array; the
+ * projection reads its elements through dynamic reads. Null when the
+ * receiver isn't a match slice or the regex isn't traceable — the
+ * caller's member fence (with the groups hint) names the gap. */
function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
if (expr.name.text !== "groups" || expr.questionDotToken !== undefined) return null;
const loc = locOf(expr);
@@ -13484,7 +13737,9 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio
return null;
}
const recv = lowerer.lowerExpr(expr.expression);
- if (!isMatchSliceType(lowerer, recv.type)) return null;
+ // A JavaScript callback parameter holds the row as a dynamic array.
+ const dynRow = recv.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile());
+ if (!dynRow && !isMatchSliceType(lowerer, recv.type)) return null;
return lowerGroupsProjection(lowerer, recv, groups, loc);
}
@@ -13530,6 +13785,21 @@ function matchSliceUnionArms(
return { arrTag, units };
}
+/** Node's TypeError for `.groups` read off null or undefined. */
+function groupsReadTypeError(unit: "null" | "undefined", loc: SrcLoc): IrStmt {
+ return {
+ kind: "throw",
+ value: {
+ kind: "libCall",
+ fn: "error.new",
+ args: [strLit(`Cannot read properties of ${unit} (reading 'groups')`, loc)],
+ type: { kind: "object", className: "%TypeError" },
+ loc,
+ },
+ loc,
+ };
+}
+
/** The groups-record projection holds string | undefined values, including
* an own overflow entry for every nonparticipating capture. One lifted helper per
* (index list, receiver representation) builds it; see
@@ -13556,7 +13826,8 @@ export function lowerGroupsProjection(
const valueType = arrayValueType(lowerer, STRING);
const shapeId = lowerer.shapes.intern([], false, valueType);
const recordT: IrType = { kind: "record", shapeId };
- const recvKey = recv.type.kind === "union" ? recv.type.unionId : "arr";
+ const recvKey =
+ recv.type.kind === "union" ? recv.type.unionId : recv.type.kind === "dyn" ? "dyn" : "arr";
const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
@@ -13580,26 +13851,7 @@ export function lowerGroupsProjection(
type: BOOL,
loc,
},
- then: [
- {
- kind: "throw",
- value: {
- kind: "libCall",
- fn: "error.new",
- args: [
- {
- kind: "strLit",
- value: `Cannot read properties of ${u.unit} (reading 'groups')`,
- type: STRING,
- loc,
- },
- ],
- type: { kind: "object", className: "%TypeError" },
- loc,
- },
- loc,
- },
- ],
+ then: [groupsReadTypeError(u.unit, loc)],
else_: null,
loc,
});
@@ -13615,10 +13867,33 @@ export function lowerGroupsProjection(
locals.push({ id: "arr.0", name: "arr", type: arrayOf(STRING), mutable: false });
body.push({ kind: "varDecl", localId: "arr.0", init: narrowed, loc });
mRef = { kind: "varRef", localId: "arr.0", type: arrayOf(STRING), loc };
+ } else if (recv.type.kind === "dyn") {
+ // A JavaScript callback parameter holds the row as a dynamic array:
+ // Node's TypeError for null and undefined, then dynamic element reads.
+ for (const unit of ["null", "undefined"] as const) {
+ body.push({
+ kind: "if",
+ cond: { kind: "dynTest", test: unit, value: mRef, type: BOOL, loc },
+ then: [groupsReadTypeError(unit, loc)],
+ else_: null,
+ loc,
+ });
+ }
}
const elem = (index: number): IrExpr =>
- arrayValueRead(lowerer, mRef, numLit(index, loc), STRING, loc);
+ mRef.type.kind === "dyn"
+ ? lowerer.coerceToExpected(
+ {
+ kind: "dynKeyGet",
+ value: mRef,
+ key: strLit(String(index), loc),
+ type: DYN,
+ loc,
+ },
+ valueType,
+ )
+ : arrayValueRead(lowerer, mRef, numLit(index, loc), STRING, loc);
const values = new Map();
order.forEach((name, i) => {
const idxs = indicesByName.get(name)!;
@@ -13656,6 +13931,7 @@ export function lowerGroupsProjection(
value: {
kind: "recordLit",
fields: order.map((name) => ({ name, value: values.get(name)!, overflow: true as const })),
+ nullProto: true,
type: recordT,
loc,
},
@@ -13885,6 +14161,28 @@ export function lowerUnionProperty(
}
return null;
}
+ // A class instance the checker sees through a wider union (`(concrete
+ // as A | B).value`): the value is that class's instance, so read its
+ // field directly.
+ if (value.type.kind === "object") {
+ const className = value.type.className;
+ const info = lowerer.classes.get(className) ?? null;
+ const field = expr.name.text;
+ const fieldType = info?.fields.get(field);
+ if (fieldType !== undefined)
+ return lowerer.fieldGetExpr(
+ { container: "class", obj: value, className, field, fieldType },
+ locOf(expr),
+ expr,
+ );
+ const getter = lowerer.findMethodOn(info, `get:${field}`);
+ if (getter)
+ return lowerer.fieldGetExpr(
+ { container: "accessor", obj: value, className, field, fieldType: getter.sig.ret },
+ locOf(expr),
+ expr,
+ );
+ }
if (value.type.kind !== "union") {
lowerer.unsupported(
"SC1090",
@@ -15551,3 +15849,20 @@ function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, loc: SrcLoc): IrE
}
return { kind: "call", callee: name, args: [source], type: DYN, loc };
}
+
+/** The `declare module "..."` body (or nested namespace body) a module
+ * export's declaration sits directly in, or undefined. */
+function builtinModuleExportParent(decl: ts.Node): ts.ModuleBlock | undefined {
+ const parent = ts.isVariableDeclaration(decl) ? decl.parent?.parent?.parent : decl.parent;
+ return parent !== undefined && ts.isModuleBlock(parent) ? parent : undefined;
+}
+
+/** A function, class or variable exported by a builtin module's body. */
+function isBuiltinModuleValueDecl(decl: ts.Node): boolean {
+ return (
+ builtinModuleExportParent(decl) !== undefined &&
+ (ts.isFunctionDeclaration(decl) ||
+ ts.isClassDeclaration(decl) ||
+ ts.isVariableDeclaration(decl))
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts
index 7afa33662..2991c0d39 100644
--- a/packages/compiler/src/frontend/lowering/lower-functions.ts
+++ b/packages/compiler/src/frontend/lowering/lower-functions.ts
@@ -168,6 +168,27 @@ export function lowerLambda(
| ts.SetAccessorDeclaration,
checkedReceiver = false,
): IrExpr {
+ // A generic lambda used as a value has no per-call instantiation: it
+ // lowers once with its type parameters bound to `unknown`, the erased
+ // instance JavaScript itself runs.
+ if (node.typeParameters?.length && !lowerer.erasingLambdas.has(node)) {
+ const previous = lowerer.typeParamBindings;
+ const bindings = new Map(previous ?? []);
+ for (const parameter of node.typeParameters) {
+ const symbol = lowerer.checker.getSymbolAtLocation(parameter.name);
+ if (symbol) bindings.set(symbol, DYN);
+ }
+ lowerer.typeParamBindings = bindings;
+ lowerer.erasingLambdas.add(node);
+ lowerer.erasingDepth++;
+ try {
+ return lowerLambda(lowerer, node, checkedReceiver);
+ } finally {
+ lowerer.erasingDepth--;
+ lowerer.erasingLambdas.delete(node);
+ lowerer.typeParamBindings = previous;
+ }
+ }
const prepared =
ts.isArrowFunction(node) || ts.isFunctionExpression(node)
? lowerer.preparedClassFactories.get(node)
diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts
index c61920c60..2eda4c4ce 100644
--- a/packages/compiler/src/frontend/lowering/lower-generators.ts
+++ b/packages/compiler/src/frontend/lowering/lower-generators.ts
@@ -32,7 +32,7 @@ export type GenType = IrType & { kind: "generator" };
/** The interned IteratorResult record of a generator type (never null for
* a MAPPED generator — mapType required it to intern). */
-function resultRecordOf(lowerer: Lowerer, genT: GenType): IrType & { kind: "record" } {
+export function resultRecordOf(lowerer: Lowerer, genT: GenType): IrType & { kind: "record" } {
const rec = genResultRecord(genT.yieldT, genT.retT, lowerer.shapes, lowerer.unions);
if (!rec)
throw new InternalCompilerError("lowerer bug: mapped generator without a result record");
diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts
index 7c08a34f9..1d67cef92 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -135,12 +135,17 @@ function inspectSupport(
case "regex":
case "symbol":
case "bigint":
+ case "date":
case "dyn":
return null;
case "array":
return inspectSupport(lowerer, t.elem, visiting, out);
case "bytes":
- return "typed arrays inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)";
+ // A Uint8Array and a Buffer share the u8 representation and print
+ // differently; the other element kinds are never Buffers.
+ return t.elem === "u8"
+ ? "Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)"
+ : null;
case "record": {
if (visiting.has(t.shapeId)) {
out.recursive = true;
@@ -321,7 +326,20 @@ export function inspectExpr(
return { kind: "libCall", fn: "sym.toString", args: [value], type: STRING, loc };
case "bigint":
return { kind: "libCall", fn: "bigint.inspect", args: [value], type: STRING, loc };
+ case "date":
+ // Node prints a Date's toISOString() at every depth, and its
+ // toString() ("Invalid Date") when the time value is NaN.
+ return { kind: "libCall", fn: "insp.date", args: [value], type: STRING, loc };
case "bytes":
+ if (t.elem !== "u8") {
+ return {
+ kind: "libCall",
+ fn: "insp.typedArray",
+ args: [value, recurse, depth],
+ type: STRING,
+ loc,
+ };
+ }
return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc };
case "dyn":
return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc };
@@ -1436,23 +1454,27 @@ export function lowerConsoleInspectArg(
}
if (t.kind === "bytes") {
// One runtime representation serves Buffer AND Uint8Array; their
- // renderings differ, so the argument's checker type picks — and only
- // Buffer lowers (lowerInspectCall's stance).
+ // renderings differ, so the argument's checker type picks
+ // (lowerInspectCall's stance).
const tname = lowerer.checker.typeToString(
lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node)),
);
- const isBuffer =
- tname === "Buffer" ||
- tname === "NonSharedBuffer" ||
- tname.startsWith("Buffer<") ||
- tname.startsWith("NonSharedBuffer<");
- if (t.elem !== "u8" || !isBuffer) {
+ if (isDataViewTypeName(tname)) {
lowerer.unsupported(
"SC1090",
node,
- `${surface} of '${tname}' values (Buffer's form is the lowered typed-array rendering; other typed arrays fence)`,
+ `${surface} of '${tname}' values (Buffer and the numeric typed arrays are the lowered byte renderings; DataView has none yet)`,
);
}
+ if (!isBufferTypeName(tname)) {
+ return {
+ kind: "libCall",
+ fn: "insp.typedArray",
+ args: [value, numLit(0, loc), numLit(2, loc)],
+ type: STRING,
+ loc,
+ };
+ }
return inspectExpr(lowerer, t, value, numLit(0, loc), numLit(2, loc), loc);
}
if (t.kind === "void") {
@@ -1464,6 +1486,10 @@ export function lowerConsoleInspectArg(
`${surface} of a void call result (call it as its own statement, then ${surface}(undefined))`,
);
}
+ // A read that always throws (an ambient name, a binding whose
+ // initializer threw) never produces a value to render.
+ if (value.kind === "libCall" && value.fn === "global.undefRead")
+ return formatValueExpr(lowerer, DYN, { ...value, type: DYN }, 2, loc);
const walk = { recursive: false };
const reason = inspectSupport(lowerer, t, new Set(), walk);
if (reason !== null) {
@@ -1472,18 +1498,41 @@ export function lowerConsoleInspectArg(
return formatValueExpr(lowerer, t, value, 2, loc);
}
+/** The checker's spelling of a Buffer, whose bytes print as . */
+function isBufferTypeName(tname: string): boolean {
+ return (
+ tname === "Buffer" ||
+ tname === "NonSharedBuffer" ||
+ tname.startsWith("Buffer<") ||
+ tname.startsWith("NonSharedBuffer<")
+ );
+}
+
+function isDataViewTypeName(tname: string): boolean {
+ return tname === "DataView" || tname.startsWith("DataView<");
+}
+
/* ── the callsite: options, direct function/class names ──────────────── */
/** The parsed options literal: the resolved depth (numeric; Infinity for
- * `null`), with every non-default knob fenced by name. */
+ * `null`), breakLength (numeric; default 80) and compact (numeric, 0 for
+ * false; default 3), with every other non-default knob fenced by name. */
+interface InspectOptions {
+ depth: number;
+ breakLength: number;
+ compact: number;
+ sorted: boolean;
+}
+
function parseInspectOptions(
lowerer: Lowerer,
node: ts.Expression | undefined,
recursive: boolean,
dir = false,
-): number {
+): InspectOptions {
const surface = dir ? "console.dir" : "util.inspect";
- if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return 2;
+ const options: InspectOptions = { depth: 2, breakLength: 80, compact: 3, sorted: false };
+ if (!node || (ts.isIdentifier(node) && node.text === "undefined")) return options;
if (!ts.isObjectLiteralExpression(node)) {
lowerer.noLowering(
`${surface} with a non-literal options argument`,
@@ -1491,7 +1540,6 @@ function parseInspectOptions(
"the options must be an object literal ({ depth, colors: false, ... }) so the knobs resolve at compile time",
);
}
- let depth = 2;
for (const prop of node.properties) {
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
lowerer.noLowering(
@@ -1522,7 +1570,7 @@ function parseInspectOptions(
// machinery ([Circular *N]) cuts every cycle, so the walk is
// bounded by the (finite) object graph — exactly Node.
void recursive;
- depth = Infinity;
+ options.depth = Infinity;
} else if (!(ts.isIdentifier(value) && value.text === "undefined")) {
const n = numeric();
if (n === null) {
@@ -1532,7 +1580,7 @@ function parseInspectOptions(
"depth must be a numeric literal (or null)",
);
}
- depth = n;
+ options.depth = n;
}
} else if (key === "colors") {
if (value.kind !== ts.SyntaxKind.FalseKeyword) {
@@ -1543,21 +1591,37 @@ function parseInspectOptions(
);
}
} else if (key === "compact") {
- if (numeric() !== 3) {
+ // Node tries the single-line form and groups arrays only for a
+ // compact of at least 1; false and smaller numbers break every
+ // composite onto its own lines. compact: true is the older layout.
+ const n = value.kind === ts.SyntaxKind.FalseKeyword ? 0 : numeric();
+ if (n === null || Number.isNaN(n)) {
lowerer.noLowering(
- `${surface} with a non-default compact option`,
+ `${surface} with this compact option`,
value,
- "only the default compact: 3 is lowered",
+ "compact must be false or a numeric literal (compact: true has no lowering)",
);
}
+ options.compact = Math.max(n, 0);
+ } else if (key === "sorted") {
+ if (value.kind !== ts.SyntaxKind.TrueKeyword && value.kind !== ts.SyntaxKind.FalseKeyword) {
+ lowerer.noLowering(
+ `${surface} with this sorted option`,
+ value,
+ "sorted must be true or false (a comparator function has no lowering)",
+ );
+ }
+ options.sorted = value.kind === ts.SyntaxKind.TrueKeyword;
} else if (key === "breakLength") {
- if (numeric() !== 80) {
+ const n = ts.isIdentifier(value) && value.text === "Infinity" ? Infinity : numeric();
+ if (n === null || Number.isNaN(n)) {
lowerer.noLowering(
- `${surface} with a non-default breakLength option`,
+ `${surface} with a non-literal breakLength option`,
value,
- "only the default breakLength: 80 is lowered",
+ "breakLength must be a numeric literal or Infinity",
);
}
+ options.breakLength = n;
} else if (dir && key === "customInspect") {
// console.dir's own default; only that value is lowered.
if (value.kind !== ts.SyntaxKind.FalseKeyword) {
@@ -1571,11 +1635,49 @@ function parseInspectOptions(
lowerer.noLowering(
`${surface} with the '${key}' option`,
prop,
- `the lowered options are depth (numeric literal or null), colors: false, compact: 3, ${dir ? "breakLength: 80, and customInspect: false" : "and breakLength: 80"}`,
+ `the lowered options are depth (numeric literal or null), colors: false, compact (false or a number), breakLength, ${dir ? "sorted (true or false), and customInspect: false" : "and sorted (true or false)"}`,
);
}
}
- return depth;
+ return options;
+}
+
+/** A render under non-default breakLength/compact/sorted: the value is evaluated
+ * first, the layout applies to this render only, and the defaults return
+ * afterwards. */
+function withInspectLayout(
+ lowerer: Lowerer,
+ options: InspectOptions,
+ value: IrExpr,
+ render: (value: IrExpr) => IrExpr,
+ loc: SrcLoc,
+): IrExpr {
+ if (options.breakLength === 80 && options.compact === 3 && !options.sorted) return render(value);
+ const input = lowerer.declareHiddenLocal("%inspectInput", value.type);
+ const text = lowerer.declareHiddenLocal("%inspectText", STRING);
+ const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "insp.layout",
+ args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)],
+ type: VOID,
+ loc,
+ },
+ loc,
+ });
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: input.id, init: value, loc },
+ layout(options.breakLength, options.compact, options.sorted),
+ { kind: "varDecl", localId: text.id, init: render(varRef(input.id, value.type, loc)), loc },
+ layout(80, 3, false),
+ ],
+ result: varRef(text.id, STRING, loc),
+ type: STRING,
+ loc,
+ };
}
/** `util.inspect(fn)` / `util.inspect(Cls)` over a DIRECT identifier
@@ -1664,33 +1766,45 @@ export function lowerInspectValue(
if (value.type.kind === "bytes") {
// One runtime representation serves Buffer AND Uint8Array; their
// renderings differ ( vs Uint8Array(1) [ 170 ]), so the
- // TOP-LEVEL argument's checker type picks — and only Buffer lowers.
- // Nested bytes fence in inspectSupport (no checker type survives
- // into shapes).
+ // TOP-LEVEL argument's checker type picks. Nested u8 bytes fence in
+ // inspectSupport (no checker type survives into shapes).
const tname = lowerer.checker.typeToString(
lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(valueNode)),
);
- const isBuffer =
- tname === "Buffer" ||
- tname === "NonSharedBuffer" ||
- tname.startsWith("Buffer<") ||
- tname.startsWith("NonSharedBuffer<");
- if (dir) {
+ const isBuffer = isBufferTypeName(tname);
+ if (dir && isBuffer) {
lowerer.noLowering(
`console.dir of '${tname}' values`,
valueNode,
"console.dir disables custom inspection, and the typed-array rendering it falls back to has no lowering yet — use console.log",
);
}
- if (value.type.elem !== "u8" || !isBuffer) {
+ if (isDataViewTypeName(tname)) {
lowerer.noLowering(
`util.inspect of '${tname}' values`,
valueNode,
- "Buffer's form is the lowered typed-array rendering; other typed arrays fence",
+ "Buffer and the numeric typed arrays are the lowered byte renderings; DataView has none yet",
);
}
- const bufDepth = parseInspectOptions(lowerer, optionsNode, false, dir);
- void bufDepth; // Buffers render fully at any depth (custom inspect)
+ const options = parseInspectOptions(lowerer, optionsNode, false, dir);
+ if (!isBuffer) {
+ // A numeric typed array: Uint8Array(n) [ ... ] under the depth and
+ // layout options.
+ return withInspectLayout(
+ lowerer,
+ options,
+ value,
+ (input) => ({
+ kind: "libCall",
+ fn: "insp.typedArray",
+ args: [input, numLit(0, loc), numLit(options.depth, loc)],
+ type: STRING,
+ loc,
+ }),
+ loc,
+ );
+ }
+ // Buffers render fully at any depth and on one line (custom inspect).
return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(2, loc), loc);
}
const walk = { recursive: false };
@@ -1698,17 +1812,30 @@ export function lowerInspectValue(
if (reason !== null) {
lowerer.noLowering(`${surface} of '${lowerer.fmt(value.type)}' values`, valueNode, reason);
}
- const depth = parseInspectOptions(lowerer, optionsNode, walk.recursive, dir);
+ const options = parseInspectOptions(lowerer, optionsNode, walk.recursive, dir);
+ const depth = options.depth;
if (dir && value.type.kind === "dyn") {
- return {
- kind: "libCall",
- fn: "console.dirDyn",
- args: [value, numLit(depth, loc)],
- type: STRING,
+ return withInspectLayout(
+ lowerer,
+ options,
+ value,
+ (input) => ({
+ kind: "libCall",
+ fn: "console.dirDyn",
+ args: [input, numLit(depth, loc)],
+ type: STRING,
+ loc,
+ }),
loc,
- };
+ );
}
- return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc);
+ return withInspectLayout(
+ lowerer,
+ options,
+ value,
+ (input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc),
+ loc,
+ );
}
/* ── util.format / util.formatWithOptions ────────────────────────────── */
@@ -1811,6 +1938,34 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S
return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc);
}
+/** %d and %i over the argument types Node formats without a number
+ * conversion: a bigint prints as `${n}n` (formatBigInt) and a symbol as NaN;
+ * null is Number(null) (0) under %d and parseInt("null") (NaN) under %i, and
+ * undefined (including a void call) is NaN under both. Other arguments
+ * still evaluate. Null for other types. */
+function formatIntegerFixed(value: IrExpr, spec: "%d" | "%i", loc: SrcLoc): IrExpr | null {
+ if (value.type.kind === "bigint") {
+ return { kind: "libCall", fn: "bigint.inspect", args: [value], type: STRING, loc };
+ }
+ let text: string;
+ if (
+ value.type.kind === "symbol" ||
+ value.type.kind === "undefinedT" ||
+ value.type.kind === "void"
+ )
+ text = "NaN";
+ else if (value.type.kind === "nullT") text = spec === "%d" ? "0" : "NaN";
+ else return null;
+ if (value.kind === "unitLit") return strLit(text, loc);
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: value, loc }],
+ result: strLit(text, loc),
+ type: STRING,
+ loc,
+ };
+}
+
/** util.format(...) / util.formatWithOptions({}, ...): the compile-time
* %-substitution over a literal format string (formatWithOptionsInternal
* ported exactly — the arg cursor, the args-exhausted guard, %c's
@@ -1880,6 +2035,8 @@ export function lowerFormatCall(
// strings through the runtime's ECMA-exact StringToNumber
// (num.fromString — the same lowering Number(aString) takes).
const value = lowerer.lowerExpr(node);
+ const fixed = formatIntegerFixed(value, "%d", loc);
+ if (fixed) return fixed;
if (value.type.kind === "f64")
return { kind: "libCall", fn: "insp.f64", args: [value], type: STRING, loc };
if (value.type.kind === "string") {
@@ -1935,6 +2092,8 @@ export function lowerFormatCall(
case 105: {
// %i — parseInt(ToString(arg)): the spec-exact composition.
const value = lowerer.lowerExpr(node);
+ const fixed = formatIntegerFixed(value, "%i", loc);
+ if (fixed) return fixed;
if (
value.type.kind === "f64" ||
value.type.kind === "bool" ||
@@ -1975,10 +2134,10 @@ export function lowerFormatCall(
return strLit("undefined", loc);
}
// A checked-dynamic argument (`console.error('headers: %j',
- // headers)` — the suite's http logging idiom): the runtime dyn
- // walk stringifies JS-exactly (root undefined/function prints
- // "undefined"; a handle in the tree throws the "not supported yet"
- // Error).
+ // headers)` — the suite's http logging idiom): the runtime runs
+ // JSON.stringify's walk (toJSON hooks included; a root it drops
+ // prints "undefined") and prints "[Circular]" for a circular
+ // structure. A typed argument prints "[Circular]" the same way.
if (value.type.kind === "dyn") {
return { kind: "libCall", fn: "insp.jsonDyn", args: [value], type: STRING, loc };
}
@@ -1989,7 +2148,7 @@ export function lowerFormatCall(
"only JSON-safe static types lower",
);
}
- return { kind: "jsonStringify", value, type: STRING, loc };
+ return { kind: "jsonStringify", value, circular: true, type: STRING, loc };
}
case 102: {
// %f — parseFloat(String(arg)) formatted: a number keeps its value
diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts
index b49cd045b..2022d8232 100644
--- a/packages/compiler/src/frontend/lowering/lower-mock.ts
+++ b/packages/compiler/src/frontend/lowering/lower-mock.ts
@@ -32,7 +32,8 @@ import {
type IrType,
type SrcLoc,
} from "../../ir/ir.js";
-import { countedFor, dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
+import { countedFor, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
+import type { IrLibFn } from "../../ir/builtin-signatures.js";
import { mockType as mockHybridType } from "../mock-shapes.js";
import { locOf } from "../program.js";
import { resultIsDiscarded } from "./call-position.js";
@@ -862,6 +863,157 @@ export function lowerMockTrackerCall(
};
}
+const MOCK_TIMERS_HINT =
+ "enable, tick, runAll, reset and setTime are the supported mock.timers members";
+const MOCK_TIMERS_APIS_HINT =
+ "list the mocked APIs as string literals: enable({ apis: ['setTimeout', 'Date'] })";
+/** The codes scr_test.c reads for the APIs enable() mocks, in Node's
+ * SUPPORTED_APIS order. */
+const MOCK_TIMERS_API_CODES: Readonly> = {
+ setTimeout: "T",
+ setInterval: "I",
+ setImmediate: "M",
+ Date: "D",
+ "scheduler.wait": "W",
+ "AbortSignal.timeout": "A",
+};
+/** enable() and enable({ now }) without `apis` mock every API. */
+const MOCK_TIMERS_ALL_APIS = "TIMDWA";
+
+/** `mock.timers.enable(...)` and the other MockTimers members, on the
+ * module's `mock` (tracker null) and t.mock. enable() takes no options or
+ * an object literal whose `apis`, when given, is an array literal of API
+ * names. */
+export function lowerMockTimersCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ member: string,
+ what: string,
+ tracker: () => MockTracker,
+ sym: ts.Symbol | null,
+): IrExpr {
+ const loc = locOf(call);
+ const args = call.arguments;
+ const optionalNumber = (fallback: number): IrExpr[] => {
+ if (args.length > 1)
+ lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call);
+ return [args.length === 1 ? lowerer.lowerExprExpecting(args[0]!, F64) : numLit(fallback, loc)];
+ };
+ if (!["enable", "tick", "setTime", "runAll", "reset"].includes(member)) {
+ lowerer.noLowering(`${what}.${member}`, call, MOCK_TIMERS_HINT, sym);
+ }
+ // The receiver evaluates before the arguments.
+ const t = tracker();
+ let rest: IrExpr[];
+ switch (member) {
+ case "enable":
+ rest = lowerMockTimersOptions(lowerer, call, what);
+ break;
+ case "tick":
+ rest = optionalNumber(1);
+ break;
+ case "setTime":
+ rest = optionalNumber(0);
+ break;
+ default:
+ if (args.length > 0) {
+ lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call);
+ }
+ rest = [];
+ }
+ const name = member[0]!.toUpperCase() + member.slice(1);
+ return {
+ kind: "libCall",
+ fn: (t ? `test.ctxMockTimers${name}` : `test.mockTimers${name}`) as IrLibFn,
+ args: t ? [t, ...rest] : rest,
+ type: VOID,
+ loc,
+ };
+}
+
+/** enable()'s { apis, now } as the libCall's API codes, `now` and flags
+ * (1 = `now` given, 2 = it is a Date). */
+function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] {
+ const loc = locOf(call);
+ const options = call.arguments[0];
+ if (call.arguments.length > 1) {
+ lowerer.noLowering(`${what}.enable with ${call.arguments.length} arguments`, call);
+ }
+ if (options === undefined) {
+ return [strLit(MOCK_TIMERS_ALL_APIS, loc), numLit(0, loc), numLit(0, loc)];
+ }
+ if (!ts.isObjectLiteralExpression(options)) {
+ lowerer.noLowering(
+ `${what}.enable with a non-literal options object`,
+ options,
+ MOCK_TIMERS_APIS_HINT,
+ );
+ }
+ let codes = "";
+ let listed = false;
+ let now: IrExpr = numLit(0, loc);
+ let flags = 0;
+ for (const prop of options.properties) {
+ // `{ apis: [...] }`, `{ "now": n }` and the `{ now }` shorthand.
+ const init = ts.isPropertyAssignment(prop)
+ ? prop.initializer
+ : ts.isShorthandPropertyAssignment(prop) && ts.isIdentifier(prop.name)
+ ? prop.name
+ : null;
+ const key =
+ init && prop.name && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
+ ? prop.name.text
+ : null;
+ if (key === "apis") {
+ const list = init!;
+ if (!ts.isArrayLiteralExpression(list)) {
+ lowerer.noLowering(
+ `${what}.enable with a non-literal apis list`,
+ list,
+ MOCK_TIMERS_APIS_HINT,
+ );
+ }
+ listed = true;
+ for (const el of list.elements) {
+ if (!ts.isStringLiteralLike(el)) {
+ lowerer.noLowering(
+ `${what}.enable with a non-literal API name`,
+ el,
+ MOCK_TIMERS_APIS_HINT,
+ );
+ }
+ const code = MOCK_TIMERS_API_CODES[el.text] ?? "";
+ if (code === "") {
+ lowerer.noLowering(`${what}.enable with the ${el.text} API`, el, MOCK_TIMERS_APIS_HINT);
+ }
+ codes += code;
+ }
+ } else if (key === "now") {
+ const value = lowerer.lowerExpr(init!);
+ if (value.type.kind === "date") {
+ now = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc };
+ flags = 3;
+ } else if (value.type.kind === "f64") {
+ now = value;
+ flags = 1;
+ } else {
+ lowerer.noLowering(
+ `${what}.enable with a now value of this type`,
+ prop,
+ "now takes a number or a Date",
+ );
+ }
+ } else {
+ lowerer.noLowering(
+ `${what}.enable with this option`,
+ prop,
+ "apis and now are the supported enable options",
+ );
+ }
+ }
+ return [strLit(listed ? codes : MOCK_TIMERS_ALL_APIS, loc), now, numLit(flags, loc)];
+}
+
/** `fn.mock.calls`: a copy of the log, as Node's getter returns. Null for
* other reads. */
export function lowerMockContextProperty(
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index 9300d19d4..d18c4f547 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -129,6 +129,7 @@ import { forkTargetPaths } from "../fork-target.js";
import { workerTargetPaths } from "../worker-target.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
import { handleEventReaderPrelude } from "./lower-event-target.js";
+import { jsHoldsDynObject } from "./lower-exprs.js";
/** One file's declarations, split for collection and init-body lowering. */
export interface FileParts {
@@ -1993,6 +1994,36 @@ export function collectGlobals(
}
continue;
}
+ // A JS file-scope ARRAY LITERAL holding an existing checked-dynamic
+ // object (a file-scope object literal binding above, directly or
+ // in a nested literal): the literal builds as a checked-dynamic
+ // array (lowerArrayLiteral), and the binding holds it, so the
+ // element keeps its identity.
+ if (
+ isJsSourceFile(sf) &&
+ ts.isIdentifier(decl.name) &&
+ nameNode === decl.name &&
+ !decl.type &&
+ !hasJsTypeAnnotation(decl) &&
+ decl.initializer !== undefined &&
+ ts.isArrayLiteralExpression(decl.initializer) &&
+ jsHoldsDynObject(lowerer, decl.initializer)
+ ) {
+ const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
+ if (symbol && !lowerer.globalsBySymbol.has(symbol)) {
+ const g: IrGlobal = {
+ id: `%g.${tag}${nsPrefix}${nameNode.text}`,
+ name: nameNode.text,
+ type: DYN,
+ mutable: isLet,
+ source: bindingSource(nameNode),
+ };
+ lowerer.globalsBySymbol.set(symbol, g);
+ lowerer.globalsList.push(g);
+ if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
+ }
+ continue;
+ }
// A JS file-scope ANY-RESIDUE binding whose VALUE provably
// lives in the checked-dynamic tree: no initializer (`let catchWarning;` —
// test/common's warning ledger), a unit initializer (`let
diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts
index 8c8639d43..54d9adeb5 100644
--- a/packages/compiler/src/frontend/lowering/lower-server.ts
+++ b/packages/compiler/src/frontend/lowering/lower-server.ts
@@ -2111,6 +2111,67 @@ function lowerNetServerMethodCall(
);
}
+/** Whether a setEncoding call can reach some net.Socket in this program:
+ * `x.setEncoding(...)` or `x["setEncoding"](...)` where `x` is a socket
+ * or an untyped value. Sockets that never get an encoding keep the Buffer
+ * chunk path; with one, Node's 'data' chunks are strings. */
+function socketEncodingReachable(lowerer: Lowerer): boolean {
+ if (lowerer.socketEncodingReachable !== null) return lowerer.socketEncodingReachable;
+ let reachable = false;
+ for (const file of lowerer.program.getSourceFiles()) {
+ if (file.isDeclarationFile || !file.text.includes("setEncoding")) continue;
+ ts.walkPreorder(file, (node) => {
+ if (reachable) return "skip";
+ if (!ts.isCallExpression(node)) return undefined;
+ const callee = node.expression;
+ const receiver =
+ ts.isPropertyAccessExpression(callee) && callee.name.text === "setEncoding"
+ ? callee.expression
+ : ts.isElementAccessExpression(callee) &&
+ ts.isStringLiteralLike(callee.argumentExpression) &&
+ callee.argumentExpression.text === "setEncoding"
+ ? callee.expression
+ : null;
+ if (receiver === null) return undefined;
+ const kind = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(receiver))?.kind;
+ if (kind === undefined || kind === "netSocket" || kind === "dyn") reachable = true;
+ return undefined;
+ });
+ if (reachable) break;
+ }
+ lowerer.socketEncodingReachable = reachable;
+ return reachable;
+}
+
+/** An inline 'data' listener whose chunk parameter is typed Buffer, for a
+ * socket that may have an encoding set: the chunk binds as a checked
+ * dynamic value, so the body sees what Node passes (a string after
+ * setEncoding, a Buffer otherwise) whatever the annotation says. Null
+ * for other listener shapes. */
+function lowerEncodedDataListener(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
+ let fn = node;
+ while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
+ if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return null;
+ if (fn.parameters.length !== 1) return null;
+ const param = fn.parameters[0]!;
+ if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
+ if (symbol === undefined) return null;
+ const previousBinding = lowerer.runtimeOptionalBindingTypes.get(symbol);
+ const previousImplicit = lowerer.implicitParamTypes;
+ const implicit = new Map(previousImplicit ?? []);
+ implicit.set(symbol, lowerer.checker.getUnknownType());
+ lowerer.runtimeOptionalBindingTypes.set(symbol, DYN);
+ lowerer.implicitParamTypes = implicit;
+ try {
+ return lowerer.lowerExpr(fn);
+ } finally {
+ if (previousBinding === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol);
+ else lowerer.runtimeOptionalBindingTypes.set(symbol, previousBinding);
+ lowerer.implicitParamTypes = previousImplicit;
+ }
+}
+
/** Method calls on net.Socket receivers: write/end/destroy/pipe and
* on/once("data" | "end" | "close" | "error" | "connect"). Null for
* other receivers. */
@@ -2565,6 +2626,27 @@ function lowerNetSocketMethodCall(
loc,
};
}
+ if (param !== null && param.kind === "bytes" && socketEncodingReachable(lowerer)) {
+ const encoded = lowerEncodedDataListener(lowerer, args[1]!);
+ // A listener defined elsewhere keeps its Buffer parameter; it takes
+ // the chunk through the checked boundary, so a string chunk fails
+ // the check instead of arriving as a Buffer.
+ const cb: IrExpr =
+ encoded ??
+ ({
+ kind: "dynCheck",
+ value: lowerSocketDynListener(lowerer, args[1]!, "data listeners"),
+ type: funcOf([DYN], VOID),
+ loc,
+ } as IrExpr);
+ return {
+ kind: "libCall",
+ fn: "net.sockOnData",
+ args: [receiver, cb, once],
+ type: VOID,
+ loc,
+ };
+ }
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 289ab8fce..ad4fd5312 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -164,7 +164,11 @@ import {
} from "./lower-exprs.js";
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
-import { lowerEnvironmentKey, lowerNativeFunctionAssignment } from "./lower-exprs.js";
+import {
+ lowerEnvironmentKey,
+ lowerNativeFunctionAssignment,
+ lowerStrictEquality,
+} from "./lower-exprs.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerUnionFieldWrite } from "./expressions/union-fields.js";
@@ -1910,44 +1914,14 @@ export function lowerDestructuringDecl(
init = { kind: "unionNarrow", unionId: init.type.unionId, tag, value: uref, type: arm, loc };
}
}
- // `const { groups } = re.exec(s)!` / `const { groups: { year } } = m`
- // — every pattern element binds `groups` off a MATCH SLICE whose
- // regex is statically known: the slice has no record shape of its
- // own, so the groups projection (lowerGroupsProjection — the same
- // record `m.groups` reads produce) wraps one level in a synthesized
- // `{ groups: ... }` record and the ordinary record destructure below
- // serves the pattern — aliases and nested patterns included. Only
- // named-group regexes take the wrap: a group-less regex destructures
- // `groups: undefined` in Node (a nested pattern then throws), which
- // the fence reports honestly instead.
- if (
- ts.isObjectBindingPattern(decl.name) &&
- decl.name.elements.length > 0 &&
- isMatchSliceType(lowerer, init.type) &&
- decl.name.elements.every((el) => {
- if (el.dotDotDotToken !== undefined || el.name === undefined) return false;
- const prop = el.propertyName;
- if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups";
- return (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) && prop.text === "groups";
- })
- ) {
- const groups = matchResultNamedGroupsOf(lowerer, decl.initializer!);
- if (groups !== null && groups.length > 0) {
- const proj = lowerGroupsProjection(lowerer, init, groups, loc);
- const outerShape = lowerer.shapes.intern(
- [{ name: "groups", type: proj.type }],
- false,
- undefined,
- ["groups"],
- );
- init = {
- kind: "recordLit",
- fields: [{ name: "groups", value: proj }],
- type: { kind: "record", shapeId: outerShape },
- loc,
- };
- }
- }
+ const groupsSource = matchGroupsPatternSource(
+ lowerer,
+ decl.name,
+ init,
+ () => matchResultNamedGroupsOf(lowerer, decl.initializer!),
+ loc,
+ );
+ if (groupsSource !== null) init = groupsSource;
const tmp = lowerer.declareHiddenLocal("%destr", init.type);
out.push({ kind: "varDecl", localId: tmp.id, init, loc });
// V8's destructuring TypeError spells STATIC sources: an identifier
@@ -1974,6 +1948,55 @@ export function lowerDestructuringDecl(
return out;
}
+/** `const { groups } = re.exec(s)!` / `const { groups: { year } } = m`
+ * — every pattern element binds `groups` off a match row whose regex is
+ * statically known. The slice (or a JavaScript callback's dynamic row)
+ * has no record shape of its own, so the groups projection
+ * (lowerGroupsProjection — the same record `m.groups` reads produce)
+ * wraps one level in a synthesized `{ groups: ... }` record and the
+ * ordinary record destructure serves the pattern — aliases and nested
+ * patterns included. Only named-group regexes take the wrap: a
+ * group-less regex destructures `groups: undefined` in Node (a nested
+ * pattern then throws), which the fence reports honestly instead. Null
+ * when the pattern or the row does not qualify. */
+export function matchGroupsPatternSource(
+ lowerer: Lowerer,
+ pattern: ts.BindingName,
+ row: IrExpr,
+ groupsOf: () => { name: string; index: number }[] | null,
+ loc: SrcLoc,
+): IrExpr | null {
+ if (
+ !ts.isObjectBindingPattern(pattern) ||
+ pattern.elements.length === 0 ||
+ !(row.type.kind === "dyn"
+ ? isJsSourceFile(pattern.getSourceFile())
+ : isMatchSliceType(lowerer, row.type)) ||
+ !pattern.elements.every((el) => {
+ if (el.dotDotDotToken !== undefined || el.name === undefined) return false;
+ const prop = el.propertyName;
+ if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups";
+ return (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) && prop.text === "groups";
+ })
+ )
+ return null;
+ const groups = groupsOf();
+ if (groups === null || groups.length === 0) return null;
+ const proj = lowerGroupsProjection(lowerer, row, groups, loc);
+ const outerShape = lowerer.shapes.intern(
+ [{ name: "groups", type: proj.type }],
+ false,
+ undefined,
+ ["groups"],
+ );
+ return {
+ kind: "recordLit",
+ fields: [{ name: "groups", value: proj }],
+ type: { kind: "record", shapeId: outerShape },
+ loc,
+ };
+}
+
/** One pattern level: emits a declaration (or global assignment) per
* bound name, recursing through hidden temps for nested patterns. */
export function lowerBindingPattern(
@@ -5033,6 +5056,23 @@ function jsObjectBindingExpands(
) {
const name = property(node.left);
if (name === null || (name !== undefined && !names.has(name))) expands = true;
+ // `state = { _tag: "Done", right }` stores an object with properties
+ // the initializer's shape lacks; later reads of them must see them.
+ const target = unwrap(node.left);
+ const value = unwrap(node.right);
+ const targetSymbol = ts.isIdentifier(target) ? lowerer.resolveValueSymbol(target) : undefined;
+ if (
+ targetSymbol &&
+ aliases.has(targetSymbol) &&
+ ts.isObjectLiteralExpression(value) &&
+ value.properties.some(
+ (field) =>
+ !(ts.isPropertyAssignment(field) || ts.isShorthandPropertyAssignment(field)) ||
+ !(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) ||
+ !names.has(field.name.text),
+ )
+ )
+ expands = true;
}
if (ts.isDeleteExpression(node) && property(node.expression) !== undefined) expands = true;
if (
@@ -5156,9 +5196,21 @@ export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt
const dk = disc.type.kind;
if (dk === "dyn") return lowerDynSwitch(lowerer, stmt, disc, labels);
if (dk === "union") return lowerUnionSwitch(lowerer, stmt, disc, labels);
- if (dk !== "f64" && dk !== "string" && dk !== "bool") {
- lowerer.unsupported("SC1090", stmt.expression, "switch on non-primitive values");
- }
+ // Objects, arrays, functions and other references match their cases
+ // with ===, which compares them by identity.
+ if (dk !== "f64" && dk !== "string" && dk !== "bool")
+ return lowerBranchSwitch(lowerer, stmt, disc, labels, (stableDisc, test, expression) =>
+ lowerStrictEquality(
+ lowerer,
+ expression,
+ stmt.expression,
+ expression,
+ stableDisc,
+ test,
+ false,
+ locOf(expression),
+ ),
+ );
// The whole case-body sequence is ONE lexical scope in JS: a bare `let`
// in one case is visible in later cases.
lowerer.scopes.push(new Map());
@@ -5250,7 +5302,7 @@ function lowerDynSwitch(
loc: locOf(expression),
};
}
- if (!lowerer.dynConvertible(test.type)) {
+ if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) {
lowerer.unsupported(
"SC1100",
expression,
diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts
index 6b2563414..d18aec305 100644
--- a/packages/compiler/src/frontend/lowering/lower-test.ts
+++ b/packages/compiler/src/frontend/lowering/lower-test.ts
@@ -21,23 +21,26 @@ import {
type IrStmt,
type IrType,
type SrcLoc,
+ BOOL,
+ DYN,
F64,
STRING,
VOID,
isUnitType,
} from "../../ir/ir.js";
-import { numLit, strLit } from "../../ir/build.js";
+import { dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
import {
lowerMockContextCall,
lowerMockContextProperty,
lowerMockFn,
lowerMockMethod,
+ lowerMockTimersCall,
lowerMockTrackerCall,
type MockTracker,
} from "./lower-mock.js";
const MOCK_HINT =
- "mock.fn, mock.method, mock.restoreAll and mock.reset are the supported mock members";
+ "mock.fn, mock.method, mock.restoreAll, mock.reset and mock.timers are the supported mock members";
/** The mock tracker members that lower, for the module's `mock` (tracker
* null) and t.mock. Null for other members. */
@@ -517,7 +520,11 @@ export function lowerNodeTestModuleCall(
);
break;
case "snapshot":
- lowerer.noLowering("test.snapshot", expr, "snapshot testing has no lowering");
+ lowerer.noLowering(
+ "test.snapshot",
+ expr,
+ "t.assert.snapshot and t.assert.fileSnapshot are the supported snapshot surface",
+ );
break;
default:
lowerer.noLowering(
@@ -556,6 +563,195 @@ export function lowerTestDirectCall(
return lowerRegistration(lowerer, expr, "test(...)", loc, 0, null);
}
+/** t.assert.snapshot(value[, options]) and t.assert.fileSnapshot(value,
+ * path[, options]), Node's SnapshotManager assertions: the value runs
+ * through options.serializers, by default Node's JSON.stringify(value,
+ * null, 2), and String() of the result is compared with
+ * assert.strictEqual against the snapshot that scr_test.c reads from
+ * ".snapshot" or `path`, or records in update mode. A throw
+ * while serializing becomes Node's ERR_INVALID_STATE. */
+function lowerSnapshotAssert(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ member: "snapshot" | "fileSnapshot",
+ testCtx: ts.Expression,
+): IrExpr {
+ const loc = locOf(call);
+ const what = `t.assert.${member}`;
+ const t = lowerer.lowerExpr(testCtx);
+ const stmts: IrStmt[] = [];
+ const slot = (name: string, type: IrType, init: IrExpr): IrExpr => {
+ const local = lowerer.declareHiddenLocal(name, type);
+ stmts.push({ kind: "varDecl", localId: local.id, init, loc });
+ return varRef(local.id, type, loc);
+ };
+ // Arguments evaluate in order before the assertion serializes, as in Node.
+ const valueNode = call.arguments[0];
+ const input = slot(
+ "%snapshotInput",
+ DYN,
+ valueNode ? lowerer.lowerExprExpecting(valueNode, DYN) : dynUndefinedExpr(loc),
+ );
+ let path: IrExpr | null = null;
+ if (member === "fileSnapshot") {
+ const pathNode = call.arguments[1];
+ path = slot(
+ "%snapshotPath",
+ STRING,
+ pathNode ? lowerer.lowerExprExpecting(pathNode, STRING) : strLit("", loc),
+ );
+ }
+ const serializers =
+ snapshotSerializers(lowerer, call.arguments[member === "snapshot" ? 1 : 2], what)?.map((fn) =>
+ slot("%snapshotSerializer", DYN, fn),
+ ) ?? null;
+ if (call.arguments.length > (member === "snapshot" ? 2 : 3)) {
+ lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call);
+ }
+ const text: IrExpr = {
+ kind: "toString",
+ operand: {
+ kind: "libCall",
+ fn: "test.snapshotSerialize",
+ args: [
+ input,
+ serializers === null
+ ? dynUndefinedExpr(loc)
+ : { kind: "dynArrLit", elems: serializers, type: DYN, loc },
+ ],
+ type: DYN,
+ loc,
+ },
+ type: STRING,
+ loc,
+ };
+ const actual = slot(
+ "%snapshotActual",
+ STRING,
+ member === "snapshot"
+ ? { kind: "libCall", fn: "test.snapshotValue", args: [text], type: STRING, loc }
+ : text,
+ );
+ const expected: IrExpr =
+ member === "snapshot"
+ ? {
+ kind: "libCall",
+ fn: "test.ctxSnapshot",
+ args: [t, actual, strLit(`${lowerer.entry.fileName}.snapshot`, loc)],
+ type: STRING,
+ loc,
+ }
+ : { kind: "libCall", fn: "test.fileSnapshot", args: [actual, path!], type: STRING, loc };
+ const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
+ return {
+ kind: "seqExpr",
+ stmts,
+ result: {
+ kind: "libCall",
+ fn: "assert.eqStr",
+ args: [actual, expected, no, no, strLit("", loc), no],
+ type: VOID,
+ loc,
+ },
+ type: VOID,
+ loc,
+ };
+}
+
+/** The serializer functions of a snapshot options object literal, as
+ * checked-dynamic values in order; null without a serializers option.
+ * Options that are not an object literal, a serializers list that is not
+ * an array literal, and other options fence. */
+function snapshotSerializers(
+ lowerer: Lowerer,
+ options: ts.Expression | undefined,
+ what: string,
+): IrExpr[] | null {
+ if (options === undefined) return null;
+ const hint = "pass the options as an object literal: { serializers: [(value) => ...] }";
+ if (!ts.isObjectLiteralExpression(options)) {
+ lowerer.noLowering(`${what} options that are not an object literal`, options, hint);
+ }
+ let fns: IrExpr[] | null = null;
+ for (const prop of options.properties) {
+ const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null;
+ if (
+ name !== "serializers" ||
+ !(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop))
+ ) {
+ lowerer.noLowering(`${what} options other than serializers`, prop, hint);
+ }
+ const list = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name;
+ if (!ts.isArrayLiteralExpression(list)) {
+ lowerer.noLowering(`${what} with a non-literal serializers list`, list, hint);
+ }
+ fns = list.elements.map((el) => {
+ if (ts.isSpreadElement(el)) lowerer.noLowering(`${what} with a spread serializer`, el, hint);
+ return lowerer.lowerExprExpecting(el, DYN);
+ });
+ }
+ return fns;
+}
+
+/** `mock.timers` (tracker null) or `t.mock.timers` on a TestContext;
+ * null for other expressions. */
+function mockTimersReceiver(
+ lowerer: Lowerer,
+ expr: ts.Expression,
+): { what: string; tracker: () => MockTracker } | null {
+ if (
+ !ts.isPropertyAccessExpression(expr) ||
+ expr.questionDotToken ||
+ expr.name.text !== "timers" ||
+ !lowerer.isStdlibMember(expr)
+ ) {
+ return null;
+ }
+ const owner = expr.expression;
+ if (ts.isIdentifier(owner)) {
+ const bi = lowerer.builtinImportOf(owner);
+ return bi && bi.module === "test" && bi.member === "mock"
+ ? { what: "mock.timers", tracker: () => null }
+ : null;
+ }
+ if (
+ ts.isPropertyAccessExpression(owner) &&
+ !owner.questionDotToken &&
+ owner.name.text === "mock" &&
+ lowerer.mapTypeOf(lowerer.typeOf(owner.expression))?.kind === "testCtx" &&
+ lowerer.isStdlibMember(owner)
+ ) {
+ const testCtx = owner.expression;
+ return { what: "t.mock.timers", tracker: () => lowerer.lowerExpr(testCtx) };
+ }
+ return null;
+}
+
+/** `mock.timers[Symbol.dispose]()` and its t.mock twin: an alias of
+ * reset(), as in Node. Null for other element-access calls. */
+export function lowerMockTimersElementCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.ElementAccessExpression,
+): IrExpr | null {
+ if (call.questionDotToken || access.questionDotToken) return null;
+ const timers = mockTimersReceiver(lowerer, access.expression);
+ if (!timers) return null;
+ const key = access.argumentExpression;
+ const isDispose =
+ ts.isPropertyAccessExpression(key) &&
+ key.name.text === "dispose" &&
+ lowerer.stdlibGlobalMember(key, "Symbol") === "dispose";
+ if (!isDispose) {
+ lowerer.noLowering(
+ `${timers.what} computed member calls`,
+ call,
+ "call timers[Symbol.dispose]()",
+ );
+ }
+ return lowerMockTimersCall(lowerer, call, "reset", timers.what, timers.tracker, null);
+}
+
/** Method-position node:test calls — one entry in lower-calls.ts's
* intrinsic chain: skip/todo/only twins on NAMED import bindings
* (`test.skip(...)` where `test` came from `{ test }`), the TestContext
@@ -634,6 +830,13 @@ export function lowerTestMethodCall(
);
if (served) return served;
}
+ // mock.timers.enable(...) and the other MockTimers members, on the
+ // module's `mock` and on t.mock.
+ const timers = mockTimersReceiver(lowerer, access.expression);
+ if (timers) {
+ const sym = lowerer.checker.getSymbolAtLocation(access.name) ?? null;
+ return lowerMockTimersCall(lowerer, call, member, timers.what, timers.tracker, sym);
+ }
// t.assert.strictEqual(...) — the callee's receiver is `t.assert` on a
// TestContext: Node's t.assert methods ARE the assert functions (bound
// to the test for its counters — not modeled; the throw is identical),
@@ -645,7 +848,10 @@ export function lowerTestMethodCall(
lowerer.mapTypeOf(lowerer.typeOf(access.expression.expression))?.kind === "testCtx" &&
lowerer.isStdlibMember(access.expression)
) {
- const served = lowerer.lowerAssertModuleCall(call, { module: "assert", member }, loc);
+ const served =
+ member === "snapshot" || member === "fileSnapshot"
+ ? lowerSnapshotAssert(lowerer, call, member, access.expression.expression)
+ : lowerer.lowerAssertModuleCall(call, { module: "assert", member }, loc);
// Each t.assert call counts against the test's plan (t.plan).
if (served)
return {
diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts
index 47e929b49..44b2b6a1d 100644
--- a/packages/compiler/src/frontend/lowering/lower-timers.ts
+++ b/packages/compiler/src/frontend/lowering/lower-timers.ts
@@ -299,6 +299,40 @@ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLo
return null;
}
+const MOCKABLE_TIMER_FNS: ReadonlySet = new Set([
+ "timers.setTimeout",
+ "timers.setTimeoutHandle",
+ "timers.setInterval",
+ "timers.clearTimeout",
+ "timers.clearInterval",
+ "timers.setImmediate",
+ "timers.clearImmediate",
+]);
+
+/** Whether a timer call's callee is an ESM named or namespace import of
+ * node:timers (destructured bindings included): Node's mock.timers leaves
+ * those bindings alone and replaces the globals. */
+function isRealTimerCallee(lowerer: Lowerer, callee: ts.Expression): boolean {
+ if (ts.isIdentifier(callee)) return lowerer.builtinImportOf(callee)?.module === "timers";
+ if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) {
+ const sym = lowerer.checker.getSymbolAtLocation(callee.expression);
+ const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined;
+ return decl !== undefined && ts.isNamespaceImport(decl);
+ }
+ return false;
+}
+
+/** Marks a lowered timer call `realTimer` when its callee is a binding
+ * mock.timers does not replace. */
+function markRealTimerCall(
+ lowerer: Lowerer,
+ expr: ts.CallExpression,
+ lowered: IrExpr | null,
+): IrExpr | null {
+ if (lowered?.kind !== "libCall" || !MOCKABLE_TIMER_FNS.has(lowered.fn)) return lowered;
+ return isRealTimerCallee(lowerer, expr.expression) ? { ...lowered, realTimer: true } : lowered;
+}
+
/** One timer call by MEMBER NAME — the shared lowering behind the ambient
* globals, the node:timers named/destructured imports, and the namespace
* form (`timers.setTimeout(...)`). Null when the member isn't a lowered
@@ -308,6 +342,15 @@ export function lowerTimersMemberCall(
expr: ts.CallExpression,
member: string,
loc: SrcLoc,
+): IrExpr | null {
+ return markRealTimerCall(lowerer, expr, lowerTimerCall(lowerer, expr, member, loc));
+}
+
+function lowerTimerCall(
+ lowerer: Lowerer,
+ expr: ts.CallExpression,
+ member: string,
+ loc: SrcLoc,
): IrExpr | null {
// Delayed timers share callback adaptation and the 1ms default. Trailing
// arguments are captured now and delivered when the callback fires.
@@ -328,10 +371,12 @@ export function lowerTimersMemberCall(
expr.arguments[0]!,
loc,
);
+ // An omitted delay is NaN, Node's undefined: the loop clamps it to
+ // 1ms, and mock.timers queues it to run at the next tick.
const ms: IrExpr =
expr.arguments.length >= 2
? lowerer.lowerExpr(expr.arguments[1]!)
- : { kind: "numLit", value: 1, type: F64, loc };
+ : { kind: "numLit", value: NaN, type: F64, loc };
if (member === "setInterval") {
return { kind: "libCall", fn: "timers.setInterval", args: [cb, ms], type: F64, loc };
}
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 9516cb62e..12deec7ad 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -21,6 +21,7 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js";
import { lowerKeyObjectCall } from "./builtins/key-object.js";
import { hashStreamOf } from "./builtins/hash-stream.js";
+import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
import { iteratorSlotAdapter } from "./iterator-adapters.js";
import type { FieldLift } from "./coercions/structural-plans.js";
@@ -57,6 +58,7 @@ import {
import {
unionRetagMappable,
narrowedRetagHelper,
+ uninhabitedArmRetagHelper,
strandedUnitTrap,
strandedCoercionTrap,
recordUnionWrapHelper,
@@ -195,6 +197,7 @@ import {
cjsExportAssignmentOf,
cjsExportDiscardReason,
fallbackDtsPath,
+ isCheckedJsSourceFile,
isCjsExportTableLiteral,
isCjsJsFile,
isJsSourceFile,
@@ -338,6 +341,7 @@ import {
} from "./call-signatures.js";
import {
implicitAnyParamSymbolsOf,
+ implicitLocalFnNodeOf,
type GenericFnInfo,
type GenericInstance,
implicitMonoFile,
@@ -413,6 +417,7 @@ import {
lowerTracingChannelMethodCall,
lowerTracingChannelProperty,
} from "./builtins/async-context.js";
+import { lowerDisposableStackNew } from "./builtins/disposable-stack.js";
import { lowerErrorCodeProperty } from "./builtins/errors.js";
import {
lowerProcessProperty,
@@ -429,7 +434,7 @@ import {
} from "./builtins/console.js";
import { lowerConsoleNew } from "./lower-console.js";
import { lowerNumberStaticCall, lowerNumberStaticProperty } from "./builtins/numbers.js";
-import { lowerDateCall } from "./builtins/dates.js";
+import { dateJsonType, isDateJsonSource, lowerDateCall } from "./builtins/dates.js";
import {
lowerFsConstantsProperty,
lowerBuiltinConstantsProperty,
@@ -519,6 +524,7 @@ import {
lowerExprStatement,
lowerForOf,
lowerForStatement,
+ matchGroupsPatternSource,
} from "./lower-stmts.js";
import {
type FieldTarget,
@@ -553,6 +559,7 @@ import {
fieldSetStmt,
lowerFieldCompound,
uniqueSymbolKeyOf,
+ matchCallbackParamGroupsOf,
} from "./lower-exprs.js";
import {
finishOptionalChain,
@@ -692,6 +699,8 @@ export interface LowerResult {
export interface LowerOptions {
/** Parser/checker services owned by the load; required for npm graph analysis. */
frontendServices?: FrontendServices | undefined;
+ /** The project's tsconfig sets `checkJs` (see isCheckedJsSourceFile). */
+ projectCheckJs?: boolean;
/** Coverage: additionally lower the unreached remainder (bodies nothing
* on the entry path reaches) in a throwaway pass and report its
* diagnostics and stats under `unreached` — the whole-program analysis
@@ -777,6 +786,7 @@ type RuntimeFenceTarget =
/** The Lowerer's pass configuration (see lowerToIr). */
export interface LowererMode {
frontendServices?: FrontendServices | undefined;
+ projectCheckJs?: boolean;
/** Names of bodies a prior reachability pass reached; null lowers everything. */
reachable?: ReadonlySet | null;
/** Coverage remainder: lower ONLY bodies outside `reachable`, skip the
@@ -901,6 +911,7 @@ export function lowerToIr(
options.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(externalTypes);
const validation = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
+ projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
ffiImports,
@@ -922,6 +933,7 @@ export function lowerToIr(
? validation
: new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
+ projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
ffiImports,
@@ -949,6 +961,7 @@ export function lowerToIr(
if (reachableEmit.requiresHistoricalOrderRelower) {
const emit = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
+ projectCheckJs: options.projectCheckJs ?? false,
reachable,
targetPlatform,
startupCrash,
@@ -1005,6 +1018,7 @@ export function lowerToIr(
if (options.coverage !== true) return result;
const remainder = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
+ projectCheckJs: options.projectCheckJs ?? false,
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
@@ -1515,6 +1529,28 @@ function panicSafe(query: () => T, fallback: T): T {
}
}
+/** The function's class expressions lower once per implicit-any instance.
+ * A function whose only implicit slots are destructured options and that
+ * is bound to a variable the implicit-instance probe rejects runs as one
+ * shared body, so its classes keep a single name. */
+function monomorphizedImplicit(
+ lowerer: Lowerer,
+ fn: ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration,
+): boolean {
+ const slots = implicitAnyParamSymbolsOf(lowerer, fn);
+ if (!slots) return false;
+ if (slots.some((slot) => slot !== null)) return true;
+ let binding: ts.Node | undefined = fn.parent;
+ while (binding && ts.isParenthesizedExpression(binding)) binding = binding.parent;
+ if (
+ (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
+ binding &&
+ ts.isVariableDeclaration(binding)
+ )
+ return implicitLocalFnNodeOf(lowerer, binding) !== null;
+ return true;
+}
+
export class Lowerer {
readonly frontendServices: FrontendServices | undefined;
readonly checker: ts.TypeChecker;
@@ -1738,6 +1774,13 @@ export class Lowerer {
/** Non-null while an instance body lowers: type-parameter symbol →
* concrete IR type, consulted inside mapType's recursion. */
typeParamBindings: Map | null = null;
+ /** Generic lambdas lowering as erased values (lowerLambda): their own
+ * type parameters are bound to `unknown`, so lambdaSignature admits
+ * them. */
+ readonly erasingLambdas = new Set();
+ /** Nesting depth of erased generic bodies being lowered (erased lambdas
+ * and erased instances). */
+ erasingDepth = 0;
localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = [];
/** The ts-level twin of typeParamBindings, non-null while a CALL-keyed
* instance body lowers: type-parameter symbol → the bound CHECKER type,
@@ -1752,6 +1795,10 @@ export class Lowerer {
* typeParamBindings — the checker has no `T` to substitute, so the
* binding rides the node-type accessor instead of mapType. */
implicitParamTypes: Map | null = null;
+ /** Whether some net.Socket in this program may get an encoding: a
+ * setEncoding call whose receiver is a socket or an untyped value.
+ * Computed once on first use (null until then). */
+ socketEncodingReachable: boolean | null = null;
/** IMPLICIT-ANY instances lowered EAGERLY at first demand (their return
* types are inferred from the body — the call site needs them settled),
* collected here for run()'s function list (the liftedFns discipline). */
@@ -1832,6 +1879,8 @@ export class Lowerer {
/** Record fields promoted to an undefined-armed union by an indexed-read
* value. The key is the concrete emitted shape and field name. */
readonly runtimeOptionalFields = new Set();
+ /** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */
+ readonly dateJsonBindings = new Set();
/** Record bindings whose flow class reaches storage outside the class:
* they keep the declared layout, so an initializer from unchecked reads
* converts once instead of the binding adopting the widened variant. */
@@ -2364,6 +2413,9 @@ export class Lowerer {
* base vtable signature is collected. Unused checked slots carry undefined. */
readonly virtualJsMethodArity = new Map();
readonly checkedVirtualJsMethods = new Set