Merge origin/main into opt/01-unions

Resolve the NullableUnions constructor next to main's mock-timer flag,
and offset the JSON schema's index-signature overflow slot of a record by
its shape's field base, like its declared fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 06:28:41 -07:00
co-authored by Claude Opus 5.5
570 changed files with 32261 additions and 3175 deletions
+1
View File
@@ -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/
+34 -2
View File
@@ -6,6 +6,40 @@ All notable changes to scriptc will be documented in this file.
<!-- release:start -->
## 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.**
<!-- release:end -->
## 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.
<!-- release:end -->
## 0.3.4
### Features
+33
View File
@@ -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;
+2 -1
View File
@@ -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",
+73 -4
View File
@@ -25,6 +25,11 @@ Contracts are type aliases imported from `@scriptc/types` with `import type`. Ty
<td>Every value stored is a signed 32-bit integer by construction.</td>
<td>The variable, field, parameter or result is stored as a 32-bit integer.</td>
</tr>
<tr>
<td><code>Int32[]</code></td>
<td>Every element stored into the array, through any reference, is a signed 32-bit integer by construction.</td>
<td>Integer arithmetic over its elements runs in 32-bit integer operations.</td>
</tr>
<tr>
<td><code>Immutable&lt;T&gt;</code></td>
<td>No code writes to the value, through any reference.</td>
@@ -120,6 +125,8 @@ scriptc recognizes a contract where it is written as the annotation of a declara
<dd>The written return type of a function, method or arrow function. <code>NoThrow&lt;Int32&gt;</code> and <code>NoThrow&lt;Immutable&lt;T&gt;&gt;</code> combine the result contracts.</dd>
<dt><code>Int32</code></dt>
<dd>Parameters, variables, class instance fields and function results whose type is a plain <code>number</code>.</dd>
<dt><code>Int32[]</code></dt>
<dd>Parameters, variables, class instance fields and function results whose type is a plain <code>number[]</code>, written <code>Int32[]</code>, <code>readonly Int32[]</code>, <code>Array&lt;Int32&gt;</code> or <code>ReadonlyArray&lt;Int32&gt;</code>.</dd>
<dt><code>Immutable&lt;T&gt;</code></dt>
<dd>Parameters, variables, class instance fields and function results.</dd>
<dt><code>Acyclic</code></dt>
@@ -128,7 +135,7 @@ scriptc recognizes a contract where it is written as the annotation of a declara
<dd>The <code>implements</code> clause of a class declaration: <code>class Node implements ProgramLifetime</code>.</dd>
</dl>
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<string, Int32[]>`, 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<T>` 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
</tr>
<tr>
<td>SC7002</td>
<td>A value stored into an <code>Int32</code> slot is not provably an int32; the hint shows the <code>| 0</code> form.</td>
<td>A value stored into an <code>Int32</code> slot, or an element stored into an array that can reach an <code>Int32[]</code> slot, is not provably an int32; the hint shows the <code>| 0</code> form.</td>
</tr>
<tr>
<td>SC7003</td>
<td>An <code>Int32</code> slot has writes scriptc cannot see.</td>
<td>An <code>Int32</code> slot has writes scriptc cannot see, or an <code>Int32[]</code> array can come from or escape to code scriptc cannot see.</td>
</tr>
<tr>
<td>SC7004</td>
+2 -2
View File
@@ -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.
+4
View File
@@ -208,6 +208,10 @@ title: "Limitations"
<include>../limitations/dns-lookup-errors.mdx</include>
### Socket data encoding
<include>../limitations/socket-data-encoding.mdx</include>
### UDP socket options
<include>../limitations/udp-socket-options.mdx</include>
+3
View File
@@ -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
+2
View File
@@ -234,6 +234,8 @@ Without `@scriptc/threads`, a scriptc executable also shares a message of untype
Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. `SCRIPTC_WORKER_STATS=1` prints, as each worker exits, how long it took to start, to run its script and to exit.
On Linux, a worker that exits without having shared an object it allocated with another thread returns all of its memory, including objects it never freed, such as `ProgramLifetime` instances, once no other worker thread is running. A worker shares objects when, for example, it sends a published value or frozen plain data, which other threads read by reference, or stores a value in an `InternTable`. The memory of a worker that shared stays allocated, as on other platforms. Set `SCR_SA_DISCARD=0` in the environment to keep the memory of every worker.
`publish` visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch.
Threads that add to one shared collection of published values while they run can use an [`InternTable`](/docs/experimental#interntable) from `@scriptc/experimental`.
+9 -1
View File
@@ -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 <code>scriptc</code> namespace.
@@ -90,11 +96,13 @@ After initialization, <code>counter_step(2)</code> returns <code>2</code>, and a
<tr><td><code>i64</code>, <code>u64</code></td><td>JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range.</td></tr>
<tr><td><code>string</code>, <code>bytes</code> parameters</td><td>Two i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values.</td></tr>
<tr><td><code>string</code>, <code>bytes</code> results</td><td>Two extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order.</td></tr>
<tr><td><code>cstring</code> parameters</td><td>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 <code>SC4012</code>.</td></tr>
<tr><td><code>cstring</code> results</td><td>An i32 result: the byte offset of NUL-terminated UTF-8 text in the result arena. A result containing U+0000 traps with <code>SC4015</code> instead of being truncated.</td></tr>
<tr><td><code>void</code></td><td>No result.</td></tr>
</tbody>
</table>
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 <code>cstring</code> callback parameter is one i32 pointer to NUL-terminated text, borrowed the same way; a value containing U+0000 traps with <code>SC4015</code>. The <code>cstring</code> class requires <code>"profile_format": 2</code>. In native libraries it is a <code>const char *</code> with the same rules.
Allocate host input buffers and output slots with <code>scriptc_alloc(size)</code>; a zero result indicates allocation failure. Release each allocation exactly once with <code>scriptc_free(pointer)</code>. Only free pointers returned by <code>scriptc_alloc</code>. These allocations remain live across application calls, initialization, and collection until freed.
@@ -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.
@@ -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.
@@ -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.
@@ -1 +1 @@
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `d` (match indices) and `v` (unicode sets) flags are unsupported: regex literals and `new RegExp` calls with a literal flag string containing them fail to compile with SC1120, and `new RegExp` with a computed flag string containing them throws an error naming the flag.
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
@@ -1 +1,2 @@
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module`, `mock.timers` and mock options throw `SC2020`.
- **Test mocks:** `mock.fn`, `mock.method`, `mock.restoreAll` and `mock.reset` compile, on the module's `mock` and on `t.mock`. `mock.method` mocks a function-valued field of a record, such as an object literal or an interface-typed object. Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`. Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`. `object.method === mock` is `false`, because the field holds a wrapper of the mock. `mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs. `enable()` takes no options or an object literal whose `apis`, when given, is an array literal. Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`. As in Node, the global timer functions, `Date.now()`, `new Date()` and `scheduler.wait` are mocked, and other calls through named and namespace imports of `node:timers` and `node:timers/promises` use real timers. As in Node, once a reset gives `scheduler.wait` back, calling it throws `ERR_INVALID_THIS`. While `scheduler.wait` is mocked, calling it with a `signal` option throws an `SC2020` error. A mock timer's handle converts to a negative number, where Node's mock `Timeout` converts to `NaN`. When two trackers have timers enabled, clearing a timer through the tracker that did not create it leaves the timer queued, where Node removes the entry at the same queue position in the clearing tracker.
@@ -1 +1,2 @@
- **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node. Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites. Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node.
- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `<entry file>.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`.
+1 -1
View File
@@ -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).
@@ -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.
@@ -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.
@@ -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.
@@ -1,4 +1,4 @@
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
@@ -3,3 +3,5 @@ Casts from values whose type is only known at runtime validate the value. For ex
Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. `SC6004` reports calls that pass such an instance to a function that casts it.
A Promise converted from `unknown` to a typed result validates its fulfilled value. A mismatch rejects with a `TypeError`. Rejection reasons and reference equality are preserved across conversions.
A non-null assertion (`value!`) checks the value when it extracts an object, array or other non-nullable member from a union. If the value holds `null` or `undefined`, the assertion throws a `TypeError`, even when the result is never used; Node.js continues until a property of the value is read.
@@ -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.
+1 -1
View File
@@ -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`.
+1 -1
View File
@@ -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.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "76b4947b60f682a7ab02",
"artifactVersion": "cade1b8e6e0c5c42fcc9",
"rowCount": 3880
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+26 -7
View File
@@ -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"] }
+2
View File
@@ -35,6 +35,8 @@ requested output.
Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host.
WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect.
Unsanitized `speed` program builds, and unsanitized `release` builds of programs small enough for one partition, pass `--import-bitcode <unit.bc>` once per runtime-pack unit the program links that ships bitcode. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted.
+181 -2
View File
@@ -10,9 +10,15 @@
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Analysis/RuntimeLibcallInfo.h"
#include "llvm/Analysis/TargetLibraryInfo.h"
#include "llvm/Bitcode/BitcodeReader.h"
#include "llvm/Bitcode/BitcodeWriter.h"
#include "llvm/Bitcode/BitcodeWriterPass.h"
#include "llvm/CodeGen/MachineDominators.h"
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/CodeGen/MachineModuleInfo.h"
#include "llvm/IR/DiagnosticInfo.h"
#include "llvm/IR/DiagnosticPrinter.h"
#include "llvm/IR/LegacyPassManager.h"
@@ -20,6 +26,9 @@
#include "llvm/IR/Verifier.h"
#include "llvm/IRReader/IRReader.h"
#include "llvm/LTO/LTO.h"
#include "llvm/Pass.h"
#include "llvm/PassInfo.h"
#include "llvm/PassRegistry.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Support/Caching.h"
#include "llvm/Support/FileSystem.h"
@@ -140,6 +149,138 @@ std::optional<EmitFailure> verify(Module &M, StringRef Code) {
return std::nullopt;
}
// Runs WebAssembly's pass that gives multiple-entry loops a single entry only
// on functions that have such a loop. The pass first records, for every
// block, every block it can reach, so its time and memory grow with the
// square of the block count even when it changes nothing: a module
// initializer of 40,000 blocks took four minutes and 22 GB in it. It changes
// a function only when two loop entries can reach each other, which a
// reducible control flow graph never has, so skipping reducible functions
// leaves the output unchanged.
class IrreducibleControlFlowOnly final : public MachineFunctionPass {
public:
static char ID;
explicit IrreducibleControlFlowOnly(FunctionPass *Fix)
: MachineFunctionPass(ID), Fix(Fix) {}
StringRef getPassName() const override { return Fix->getPassName(); }
bool doInitialization(Module &M) override {
MachineFunctionPass::doInitialization(M);
return Fix->doInitialization(M);
}
bool doFinalization(Module &M) override { return Fix->doFinalization(M); }
void getAnalysisUsage(AnalysisUsage &Usage) const override {
Fix->getAnalysisUsage(Usage);
}
bool runOnMachineFunction(MachineFunction &MF) override {
if (isReducible(MF))
return false;
// The wrapped pass reads the machine function through the module's
// machine code analysis, which this pass requires on its behalf.
if (Fix->getResolver() == nullptr) {
auto *Resolver = new AnalysisResolver(getResolver()->getPMDataManager());
Resolver->addAnalysisImplsPair(
&MachineModuleInfoWrapperPass::ID,
&getAnalysis<MachineModuleInfoWrapperPass>());
Fix->setResolver(Resolver);
}
return Fix->runOnFunction(MF.getFunction());
}
private:
// A graph is reducible when every edge that a depth-first search finds
// going back to a block on its stack targets a block that dominates the
// edge's source. Blocks the entry cannot reach count as irreducible, since
// the wrapped pass also examines them.
static bool isReducible(MachineFunction &MF) {
MachineDominatorTree Dominators(MF);
enum class State : unsigned char { New, Active, Done };
std::vector<State> States(MF.getNumBlockIDs(), State::New);
SmallVector<std::pair<MachineBasicBlock *,
MachineBasicBlock::succ_iterator>,
32>
Stack;
size_t Visited = 1;
MachineBasicBlock *Entry = &MF.front();
States[Entry->getNumber()] = State::Active;
Stack.push_back({Entry, Entry->succ_begin()});
while (!Stack.empty()) {
auto &[Block, Next] = Stack.back();
if (Next == Block->succ_end()) {
States[Block->getNumber()] = State::Done;
Stack.pop_back();
continue;
}
MachineBasicBlock *Successor = *Next++;
State &Seen = States[Successor->getNumber()];
if (Seen == State::Active) {
if (!Dominators.dominates(Successor, Block))
return false;
} else if (Seen == State::New) {
Seen = State::Active;
++Visited;
Stack.push_back({Successor, Successor->succ_begin()});
}
}
return Visited == MF.size();
}
std::unique_ptr<FunctionPass> Fix;
};
char IrreducibleControlFlowOnly::ID = 0;
// The code generation pipeline for one module, with two changes for
// WebAssembly. The irreducible control flow pass runs only where it can
// change the function (see IrreducibleControlFlowOnly). Object emission also
// leaves out the last machine pass, a module pass that records the external
// symbols every function uses so that assembly output can declare them
// before the first function. A module pass in the middle of the pipeline
// keeps the machine code of every function alive until all of them are
// compiled. Object files do not contain those declarations, and the printer
// creates each symbol when it lowers the first use, so without the pass each
// function's machine code is freed once it is printed.
class CodeGenPasses final : public legacy::PassManagerBase {
public:
CodeGenPasses(const TargetMachine &Machine, CodeGenFileType Type)
: Wasm(Machine.getTargetTriple().isWasm()),
Object(Type == CodeGenFileType::ObjectFile) {}
void add(Pass *P) override {
if (Wasm) {
StringRef Argument = passArgument(*P);
if (Argument == "wasm-fix-irreducible-control-flow") {
Passes.add(
new IrreducibleControlFlowOnly(static_cast<FunctionPass *>(P)));
return;
}
if (Object && Argument == "wasm-mclower-prepass") {
delete P;
return;
}
}
Passes.add(P);
}
void run(Module &M) { Passes.run(M); }
private:
static StringRef passArgument(const Pass &P) {
const PassInfo *Info =
PassRegistry::getPassRegistry()->getPassInfo(P.getPassID());
return Info == nullptr ? StringRef() : Info->getPassArgument();
}
bool Wasm;
bool Object;
legacy::PassManager Passes;
};
// Writes code for an optimized module to a unique sibling of OutputPath,
// which is appended to Temporaries once it exists.
std::optional<EmitFailure>
@@ -153,7 +294,7 @@ generateCode(Module &M, TargetMachine &Machine, CodeGenFileType Type,
return EmitFailure{"output_open_failed", EC.message()};
Temporaries.push_back(TemporaryPath);
raw_fd_ostream Output(TemporaryFd, true);
legacy::PassManager CodeGeneration;
CodeGenPasses CodeGeneration(Machine, Type);
if (Machine.addPassesToEmitFile(CodeGeneration, Output, nullptr, Type))
return EmitFailure{"emission_not_supported",
"target does not support the requested file type"};
@@ -325,7 +466,7 @@ compileUnoptimizedPartitions(const EmitOptions &Options,
P.PB.buildPerModuleDefaultPipeline(OptimizationLevel::O0)
.run(**Mod, P.MAM);
raw_svector_ostream Output(Objects[I]);
legacy::PassManager CodeGeneration;
CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
CodeGenFileType::ObjectFile))
return Failures.set({"emission_not_supported",
@@ -437,6 +578,44 @@ compileOptimizedPartitions(const EmitOptions &Options,
return true;
};
// The backend's own code generation cannot use CodeGenPasses, so
// WebAssembly partitions are compiled here with the passes it would add,
// and returning false skips it.
if (TargetTriple.isWasm())
Config.PreCodeGenModuleHook = [&](unsigned Task, const Module &Optimized) {
size_t I = Task - 1;
if (I >= Count)
return true;
std::string LookupError;
std::unique_ptr<TargetMachine> Machine =
createTargetMachine(Options.Target, Options.OptLevel, LookupError);
if (!Machine) {
Failures.set({"target_machine_failed", LookupError});
return false;
}
// The backend passes the module it owns; only the hook's signature is
// const.
Module &Mod = const_cast<Module &>(Optimized);
Objects[I].clear();
raw_svector_ostream Output(Objects[I]);
CodeGenPasses CodeGeneration(*Machine, CodeGenFileType::ObjectFile);
TargetLibraryInfoImpl LibraryInfo(Mod.getTargetTriple(),
Machine->Options.VecLib);
CodeGeneration.add(new TargetLibraryInfoWrapperPass(LibraryInfo));
CodeGeneration.add(new RuntimeLibraryInfoWrapper(
Mod.getTargetTriple(), Machine->Options.ExceptionModel,
Machine->Options.FloatABIType, Machine->Options.EABIVersion,
Machine->Options.MCOptions.ABIName, Machine->Options.VecLib));
if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr,
CodeGenFileType::ObjectFile)) {
Failures.set({"emission_not_supported",
"target does not support object emission"});
return false;
}
CodeGeneration.run(Mod);
return false;
};
// The link refers to module identifiers for its whole lifetime.
std::vector<std::string> Names(Count);
for (size_t I = 0; I < Count; ++I)
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+135 -3
View File
@@ -708,6 +708,7 @@ interface MessageEvent<T = unknown> 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<T, void, undefined>;
export const scheduler: {
wait(delay?: number): Promise<void>;
wait(delay?: number, options?: TimerOptions): Promise<void>;
yield(): Promise<void>;
};
}
@@ -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<unknown, unknown>;
@@ -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<string | number | boolean>
| bigint
| ReadonlyArray<string | number | boolean | bigint>
| 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 "<ip>[:<port>]" */
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<void>;
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<unknown[]>;
/* 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;
}
+1 -1
View File
@@ -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",
@@ -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",
@@ -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;
@@ -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`);
@@ -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]));
});
@@ -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<string, Parameter[]>();
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;
}
@@ -159,6 +159,7 @@ export const BYTES_ELEM_NUM: Record<IrBytesElem, number> = {
u16: 6,
i16: 7,
u8c: 8,
f16: 9,
};
/** The payload marker of an ABSENT record field slot (IR fieldAbsent): the
@@ -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);
@@ -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<string, IrExpr>();
const writes = new Set<string>();
// 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<string, string | null>();
function parameterTarget(fn: IrFunction, index: number, seen: Set<string>): 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<string, string>();
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);
});
}
@@ -3,6 +3,7 @@ import {
BOOL,
F64,
STRING,
UNDEFINED_T,
VOID,
arrayOf,
type IrExpr,
@@ -23,11 +24,17 @@ const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({
});
const index = ref("i", F64);
function module(name: string, returnType: IrExpr["type"], body: IrStmt[]): IrModule {
function module(
name: string,
returnType: IrExpr["type"],
body: IrStmt[],
unions: IrModule["unions"] = [],
): IrModule {
return {
irVersion: 16,
sourceFile: loc.file,
entry: "__main",
unions,
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
{
@@ -174,6 +181,76 @@ test.each([32, 64] as const)(
},
);
test.each([32, 64] as const)(
"pop removes the last element of a packed array inline and keeps a cold runtime pop (%i-bit)",
(bits) => {
const maybe = { kind: "union", unionId: "maybe" } as const;
const pop: IrExpr = {
kind: "arrIntrinsic",
method: "pop",
receiver: ref("a", nums),
args: [],
type: maybe,
loc,
};
const llvm = emitLlvmModule(
module(
"take",
maybe,
[{ kind: "return", value: pop, loc }],
[{ id: "maybe", arms: [F64, UNDEFINED_T] }],
),
{ pointerBits: bits },
);
const fn = body(llvm, "take");
const size = bits === 32 ? "i32" : "i64";
// An empty array answers undefined without a call.
expect(fn).toMatch(new RegExp(`icmp eq ${size} %t\\d+, 0\\n br i1 %t\\d+, label %remove\\.a`));
expect(fn).toMatch(new RegExp(`sub nuw ${size} %t\\d+, 1`));
expect(fn).toMatch(/store i64 0, ptr /);
expect(fn).toMatch(/store i8 0, ptr /);
expect(fn).toMatch(/phi i64 /);
expect(fn).toMatch(/call zeroext i8 @scr_arr_pop_state\(.*\) cold$/m);
},
);
test("a discarded pop releases the removed element inline", () => {
const words = ref("s", arrayOf(STRING));
const length: IrExpr = {
kind: "arrIntrinsic",
method: "length",
receiver: words,
args: [],
type: F64,
loc,
};
const llvm = emitLlvmModule(
module("drop", VOID, [
{
kind: "arraySetLength",
arr: words,
length: {
kind: "bin",
op: "-",
left: length,
right: { kind: "numLit", value: 1, type: F64, loc },
type: F64,
loc,
},
loc,
pop: true,
},
]),
);
const fn = body(llvm, "drop");
expect(fn).toMatch(/fsub double %t\d+, 1\.0/);
expect(fn).toMatch(/load i64, ptr .*\n(?:.*\n)*? store i64 0, ptr /);
expect(fn).toMatch(/inttoptr i64 %t\d+ to ptr/);
expect(fn).toContain("call void @scr_str_release(");
// Holes, sparse storage, and every other length keep the runtime call.
expect(fn).toContain("call void @scr_arr_set_len(");
});
test.each([32, 64] as const)(
"indices below the hole-free prefix bound skip the state bytes (%i-bit)",
(bits) => {
@@ -508,6 +508,75 @@ export function emitDenseArraySet(
B.startBlock(join);
}
/** The dense half of `pop`. An empty array branches to `empty` (the
* runtime's pop returns a hole state there and changes nothing). When the
* hole-free prefix bound equals the length, the last element is a present
* value in dense storage, and the runtime's pop is exactly: clear its slot
* and state, lower the bound and the length to its index, and move the
* slot out. That block is left open and its slot bits are returned as an
* i64 (a boolean's 0 or 1, zero-extended); every other array branches to
* `slow`, which keeps the runtime call. */
export function emitDenseArrayPop(
host: LlvmEmitterContext,
array: string,
acc: ArrayAccess,
empty: string,
slow: string,
): string {
const B = host.B;
const size = host.sizeType;
const header = (field: number): string => {
const ptr = B.tmp();
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
host.markMemoryPointer(ptr, "array:header");
return ptr;
};
const lenPtr = header(SCR_ARR_FIELD.len),
len = B.tmp(),
none = B.tmp();
B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
B.line(`${none} = icmp eq ${size} ${len}, 0`);
const check = B.newLabel("arr.pop.check"),
dense = B.newLabel("arr.pop.dense");
B.condBr(none, empty, check);
B.startBlock(check);
const filledPtr = header(SCR_ARR_FIELD.filled),
filled = B.tmp(),
packed = B.tmp();
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`${packed} = icmp eq ${size} ${sizedFilled(host, filled)}, ${len}`);
B.condBr(packed, dense, slow);
B.startBlock(dense);
const last = B.tmp();
B.line(`${last} = sub nuw ${size} ${len}, 1`);
const dataPtr = header(SCR_ARR_FIELD.data),
data = B.tmp(),
statesPtr = header(SCR_ARR_FIELD.present),
states = B.tmp();
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
const valuePtr = valuePointer(host, { offset: last, data }, acc);
const statePtr = statePointer(host, { offset: last, states });
const slotTy = slotType(acc);
let raw = B.tmp();
B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
if (slotTy === "i8") {
const wide = B.tmp();
B.line(`${wide} = zext i8 ${raw} to i64`);
raw = wide;
}
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
let last32 = last;
if (size === "i64") {
last32 = B.tmp();
B.line(`${last32} = trunc i64 ${last} to i32`);
}
B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
return raw;
}
/** `scr_arr_push_*` (the array takes the value's reference) returning the
* new length as a double. When `len < cap` the runtime's append is exactly
* a dense store at `len` plus `len + 1`: no sparse entry can exist past the
+184 -97
View File
@@ -1108,11 +1108,14 @@ export class LlDyn {
private readonly recordNameTables = new Map<string, { names: string; lens: string }>();
/** 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<string>): 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<string>, 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<string>): 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 `<name>_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 } => {
+487 -42
View File
@@ -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<number> = 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<string>();
/** 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<string>();
/** Fiberless async functions a stackless coroutine body awaits directly;
* each gets an inline await entry (emitCoroInlineAwait). */
readonly inlineAwaitCallees = new Set<string>();
/** The async functions whose bodies never suspend and that a direct
* await may enter in place (selectStacklessAsync). */
private inlineAwaitable = new Set<string>();
/** 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<string, string>();
@@ -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<IrExpr>();
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<string> {
const parks = this.parkingFunctions();
const eligible = parks === null ? new Set<string>() : 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<string>): Set<string> {
const eligible = new Set<string>();
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<string, string>();
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<string> | 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<string, Set<string>>();
const parks = new Set<string>();
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<string, string> | null = null;
/** The module functions whose ordinary or borrowed body `text` mentions. */
private *mentionedFunctions(text: string): Generator<string> {
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<IrExpr, string>();
/** 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<IrExpr, number> | null = null;
private currentFunctionBody: IrStmt[] = [];
private varRefCount(e: IrExpr): number {
if (this.varRefCounts === null) {
const counts = new Map<IrExpr, number>();
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<LlEmitter["binding"]>,
): 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 {
@@ -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) {
@@ -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<string, { kind: number; le: boolean } | undefined> = {
u8: { kind: 0, le: false },
@@ -43,6 +44,7 @@ const DV_GET_KIND: Record<string, number> = {
dvGetFloat64: 7,
dvGetBigUint64Number: 8,
dvGetBigInt64Number: 9,
dvGetFloat16: 10,
};
const DV_SET_KIND: Record<string, number> = {
@@ -54,6 +56,7 @@ const DV_SET_KIND: Record<string, number> = {
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
@@ -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();
@@ -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;
@@ -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 =
@@ -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 };
}
@@ -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);
@@ -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();
@@ -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") {
@@ -37,7 +37,7 @@ export const ASSERT_VERDICT_LIB_FNS: ReadonlyMap<string, number | null> = new Ma
export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"os.userHomedir": "scr_os_user_homedir",
"os.tmpdir": "scr_os_tmpdir",
"process.argv": "scr_process_argv",
"process.execArgv": "scr_process_exec_argv",
"process.platform": "scr_process_platform",
"process.cwd": "scr_process_cwd",
"process.pid": "scr_process_pid",
@@ -477,6 +480,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
// loop gate already runs) and diagnostics_channel (scr_dc.c): all
// generic shapes; the throwing members ride the pending check.
"tp.setTimeout": "scr_tp_set_timeout",
"tp.schedulerWait": "scr_tp_set_timeout",
"tp.setImmediate": "scr_tp_set_immediate",
"tp.setTimeoutValue": "scr_tp_set_timeout_value",
"tp.setImmediateValue": "scr_tp_set_immediate_value",
"tp.setTimeoutSignal": "scr_tp_set_timeout_signal",
"tp.schedulerWaitValue": "scr_tp_set_timeout_value",
"tp.schedulerWaitSignal": "scr_tp_set_timeout_signal",
"tp.setImmediateSignal": "scr_tp_set_immediate_signal",
// stream/promises' finished form (+1 promise; may-throw pending check;
// the pipeline form needs the stream-array compound and emits below).
"sp.finished": "scr_sp_finished",
@@ -1315,6 +1344,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"timers.immediatePromise": "scr_immediate_promise",
};
/** The mock-aware entry points (scr_test.c) that the timer calls of a
* program using mock.timers emit in place of the loop's functions; each
* falls back to the loop's function while its API is not mocked. */
export const MOCK_TIMER_SYMS: Readonly<Record<string, string>> = {
"timers.setTimeout": "scr_test_mt_set_timeout",
"timers.setTimeoutHandle": "scr_test_mt_set_timeout_handle",
"timers.setInterval": "scr_test_mt_set_interval",
"timers.clearTimeout": "scr_test_mt_clear_timeout",
"timers.clearInterval": "scr_test_mt_clear_interval",
"timers.unref": "scr_test_mt_unref",
"timers.ref": "scr_test_mt_ref",
"timers.refresh": "scr_test_mt_refresh",
"timers.hasRef": "scr_test_mt_has_ref",
"timers.setImmediate": "scr_test_mt_set_immediate",
"timers.clearImmediate": "scr_test_mt_clear_immediate",
"timers.immediateUnref": "scr_test_mt_immediate_unref",
"timers.immediateRef": "scr_test_mt_immediate_ref",
"timers.immediateHasRef": "scr_test_mt_immediate_has_ref",
"date.now": "scr_test_mt_date_now",
"date.newNow": "scr_test_mt_date_now",
"tp.schedulerWait": "scr_test_mt_scheduler_wait",
"tp.schedulerWaitValue": "scr_test_mt_scheduler_wait_value",
"tp.schedulerWaitSignal": "scr_tp_scheduler_wait_signal",
};
/** The mock-aware symbol for a timer call of a program that uses
* mock.timers, undefined for other calls and for `realTimer` calls. */
export function mockTimerSym(
host: { readonly mockTimers: boolean },
e: { fn: string; realTimer?: true },
): string | undefined {
return host.mockTimers && !e.realTimer ? MOCK_TIMER_SYMS[e.fn] : undefined;
}
export const USES_TIMERS_LIB_FNS = new Set<string>([
"process.emitWarning",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
@@ -1384,6 +1450,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"worker.parentPort",
"worker.channel",
"worker.receiveMessageOnPort",
"worker.broadcastChannel",
"process.stdio",
"fetch.start",
"fetch.input",
@@ -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}`);
}
@@ -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();
});
@@ -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<string> } {
const rest = stmts.slice(index + 1);
const derived = new Set<string>([localId]);
const assigned = new Set<string>();
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<string>,
assigned: ReadonlySet<string>,
): 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<string>,
assigned: ReadonlySet<string>,
): 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<string>): 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<string>): 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<string>): 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<string>): 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),
);
}
@@ -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);
});
@@ -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<string>();
private currentLocals = new Set<string>();
private currentPrivateArrays: ReadonlySet<string> = new Set();
private constructors: ReadonlySet<string> = 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<string>): void {
private stmtWrites(
s: IrStmt,
sink: WriteSink,
boxed: ReadonlySet<string>,
privates: ReadonlySet<string>,
): 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<string>): 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<string>): ReadonlySet<string> {
if (fn.async || fn.generator) return new Set();
const candidates = new Set<string>();
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<string, number>();
const rejected = new Set<string>();
// Permitted positions per varRef node (lowering may share one node
// between several positions, so each visit consumes one permission).
const permitted = new Map<IrExpr, number>();
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<string>();
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<string> {
+26 -26
View File
@@ -64,9 +64,6 @@ export class LlWalkers {
private get S(): "i32" | "i64" {
return this.host.sizeType;
}
private abiOffset(native64: number, wasm32: number): number {
return this.S === "i32" ? wasm32 : native64;
}
/** The value-parameter LLVM type of a writer for `t`. */
private valTy(t: IrType): string {
@@ -220,10 +217,10 @@ export class LlWalkers {
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`);
break;
case "dyn":
// Overflow values under an `unknown` index signature: the checked-dynamic tree
// serializes itself (runtime walker). Bare stringify of dyn stays
// frontend-fenced; this writer is reachable only through overflow
// entries.
// A checked-dynamic value serializes itself (runtime walker).
// Bare stringify of dyn stays frontend-fenced, and overflow
// entries under an `unknown` index signature go through
// scr_jb_put_dyn_entry, which also runs the value's toJSON.
this.host.declare(`declare void @scr_jb_put_dyn(ptr, ptr)`);
B.line(`call void @scr_jb_put_dyn(ptr %b, ptr %v)`);
break;
@@ -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<string>()): boolean {
if (traceAdapter(this.host, t) !== null) return true;
switch (t.kind) {
case "dyn":
return true;
case "array":
return this.jsonWriteMayThrow(t.elem, seen);
case "union":
@@ -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)}`);
@@ -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 === "-")) {
+6
View File
@@ -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)}`;
}
@@ -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";
@@ -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",
);
});
@@ -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");
@@ -129,6 +129,7 @@ const WASI_UNAVAILABLE_HOST_FACTS: ReadonlyMap<string, string> = 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)"],
+28 -6
View File
@@ -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<string, ContractNote>(); // fn
private readonly locals = new Map<string, ContractNote>(); // 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<string, IntegerRanges>();
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);
});
}
}
File diff suppressed because it is too large Load Diff
@@ -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<string, UnsupportedEntry> = {
SC1120: {
feature: "this regex feature",
milestone: "later",
hint: "supported: literal regexes with the g/i/m/s/u/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates",
hint: "supported: literal regexes with the g/i/m/s/u/v/y flags — .test(), .exec()/.match()/.matchAll(), named capture groups (.groups, $<name> templates, \\k<name>), .source/.flags, and string replace/replaceAll/split with string replacement templates",
},
// SC1121 (stateful g/y regex tests) shipped — code retired, do not reuse.
};
@@ -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);
});
+62 -12
View File
@@ -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<string, LineIndex>();
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");
}
@@ -30,6 +30,9 @@ export interface ContractNote {
loc: SrcLoc;
/** The annotated declaration's name, for messages. */
name: string;
/** `Int32[]` (also `readonly Int32[]`, `Array<Int32>`, `ReadonlyArray<Int32>`):
* 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<T>"
: 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<Int32>, ReadonlyArray<Int32>): 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<Int32> / NoThrow<Immutable<T>>: 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);
@@ -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<A & B>`) 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
) {
@@ -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(
@@ -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<Record<string, IrLibFn>> = {
getFullYear: "date.getFullYear",
@@ -24,7 +34,7 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
};
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)
);
}
@@ -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,
};
}
@@ -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<string> = 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,
};
}
@@ -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 =
@@ -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);
}
@@ -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<NetAddressClass, ReadonlySet<string>> = {
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),
};
}
@@ -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(
@@ -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.
@@ -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<T>(f) with f returning T or Promise<T>",
);
}
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<void>; a PROMISE argument is returned as-is (the spec's
@@ -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 };
}
@@ -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"
);
}
@@ -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);
@@ -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 };
}
@@ -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
@@ -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,
File diff suppressed because it is too large Load Diff

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