Merge main

This commit is contained in:
Chris Tate
2026-10-25 09:42:07 -05:00
2254 changed files with 181445 additions and 14326 deletions
+21 -3
View File
@@ -216,9 +216,24 @@ jobs:
# Start the corpus immediately beside a serial side queue. One shared
# worker budget covers both processes; file and case shard axes stay
# separate so every fixture still runs exactly once in each lane.
# The tests run in a transient scope capped below the runner's 16 GB, so
# the runner itself keeps memory: past 15 GB of RAM the scope swaps, as
# the whole runner did before, and when RAM and swap run out the kernel
# kills the scope's largest process. A sanitized shard peaks near 16 GB,
# and the sanitized self-hosting native-frontend build alone needs 15 GB
# (its clang compile of the frontend takes 10.5 GB).
# OOMPolicy=continue leaves the rest of the scope running, so vitest
# reports the killed worker and the job log names the test. Node sizes
# its default heap from the memory it may use, which the scope lowers
# below 4 GB; the workers keep the 4 GB heap they had on the bare
# runner.
- name: Tests (corpus and integration suites)
run: |
ulimit -c 0
if [ -d /run/systemd/system ]; then
exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=15G -p OOMPolicy=continue \
--uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" NODE_OPTIONS="--max-old-space-size=4096" node scripts/ci-test.mjs
fi
node scripts/ci-test.mjs
tests_macos:
@@ -524,7 +539,9 @@ jobs:
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
name: test (tsgo and tsc-ts give the same IR)
runs-on: ubuntu-24.04
timeout-minutes: 30
# The two corpus compiles run back to back and take close to 30 minutes on a
# slow hosted runner, so a 30-minute limit cancels otherwise healthy runs.
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@@ -618,8 +635,9 @@ jobs:
# Shard 1 additionally runs package/install and link-parity contracts
# before its LLVM differential slice. The slice itself can take ~10
# minutes on a cold hosted runner, so 20 minutes cancels an otherwise
# healthy job before the differential completes.
timeout-minutes: 35
# healthy job before the differential completes. Slow runners need more
# headroom than that, so the limit is 50 minutes.
timeout-minutes: 50
strategy:
fail-fast: false
matrix:
+41
View File
@@ -0,0 +1,41 @@
name: Prune caches
# Pull request caches are scoped to that pull request, so once it is closed no
# run can use them again. Deleting them keeps the repository under its cache
# size limit, so caches that main and open pull requests use are not evicted.
on:
pull_request:
types: [closed]
schedule:
- cron: "17 6 * * *"
workflow_dispatch:
permissions:
actions: write
pull-requests: read
jobs:
prune:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Delete caches of closed pull requests
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Read both lists in full before deleting anything: deleting while the cache
# listing pages shifts later pages, and a failed request must stop the job
# instead of leaving open pull requests out of the list.
open=$(gh api --paginate "repos/$REPO/pulls?state=open&per_page=100" --jq '.[].number')
caches=$(gh api --paginate "repos/$REPO/actions/caches?per_page=100" \
--jq '.actions_caches[] | select(.ref | startswith("refs/pull/")) | "\(.id) \(.ref)"')
while read -r id ref; do
[ -n "$id" ] || continue
number=$(echo "$ref" | cut -d/ -f3)
if ! grep -qx "$number" <<<"$open"; then
gh api --method DELETE "repos/$REPO/actions/caches/$id" >/dev/null
echo "deleted cache $id ($ref)"
fi
done <<<"$caches"
+88 -2
View File
@@ -100,10 +100,59 @@ jobs:
if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`);
NODE
build-native-packages:
name: Build native package (${{ matrix.platform }})
# Collects the native compiler's profile on this commit
# (scripts/refresh-cli-profile.mjs), so every platform's CLI build optimizes
# with a profile of the code it compiles. The committed profile falls behind
# main within a day or two. If this job fails, the CLI builds use the
# committed profile.
cli-profile:
name: Collect the native compiler profile
needs: check-release
if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
continue-on-error: true
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11.1.3
- uses: actions/setup-node@v4
with:
node-version: "24"
cache: pnpm
- uses: ./.github/actions/llvm-linux
- run: pnpm install --frozen-lockfile
- uses: ./.github/actions/zig
- uses: ./.github/actions/native-packages
with:
target: linux-x64-gnu
- run: pnpm --filter @scriptc/compiler --filter scriptc build
- name: Check out tsc-ts
run: |
git clone --quiet --depth 1 https://github.com/vercel-labs/tsc-ts "$RUNNER_TEMP/tsc-ts"
echo "tsc-ts $(git -C "$RUNNER_TEMP/tsc-ts" rev-parse HEAD)"
npm ci --prefix "$RUNNER_TEMP/tsc-ts"
- name: Collect the profile
run: >-
node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4
--output "$RUNNER_TEMP/cli-profile/cli.profdata.gz"
- uses: actions/upload-artifact@v4
with:
name: cli-profile
path: ${{ runner.temp }}/cli-profile/cli.profdata.gz
retention-days: 7
if-no-files-found: error
build-native-packages:
name: Build native package (${{ matrix.platform }})
needs: [check-release, cli-profile]
# Runs when the profile job fails; the CLI builds then use the committed profile.
if: >-
!cancelled() && needs.check-release.outputs.should_release == 'true' &&
needs.check-release.outputs.reuse_artifacts != 'true'
strategy:
fail-fast: false
matrix:
@@ -243,6 +292,28 @@ jobs:
- name: Build host helper for cross-libc bootstrap
if: matrix.seed_helper != ''
run: pnpm --filter @scriptc/${{ matrix.seed_helper }} build:native
- name: Download the profile collected on this commit
id: cli-profile
if: matrix.cli != ''
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: cli-profile
path: ${{ runner.temp }}/cli-profile
# Without the artifact, SCRIPTC_NATIVE_PROFILE_USE stays unset and
# build-native-cli.mts uses the committed profile.
- name: Select the native compiler profile
if: matrix.cli != ''
shell: bash
env:
COLLECTED: ${{ steps.cli-profile.outcome == 'success' && format('{0}/cli-profile/cli.profdata.gz', runner.temp) || '' }}
run: |
if [ -n "$COLLECTED" ]; then
echo "SCRIPTC_NATIVE_PROFILE_USE=$COLLECTED" >> "$GITHUB_ENV"
echo "The native CLI builds with the profile collected on $GITHUB_SHA"
else
echo "::warning::No profile was collected on this commit; the native CLI builds with the committed profile"
fi
- name: Build native CLI distribution
if: matrix.cli != ''
env:
@@ -558,6 +629,21 @@ jobs:
- name: Package standalone compilers
run: node scripts/package-native-cli.mjs packages /tmp/scriptc-release-assets
# The profile the native CLIs were built with. Runs that reuse native
# builds from before the cli-profile job, or whose profile job failed,
# have none; their CLIs used the committed profile.
- name: Download the native compiler profile
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN: ${{ needs.check-release.outputs.artifact_run }}
VERSION: ${{ needs.check-release.outputs.version }}
run: |
if gh run download "$RUN" --repo "$GITHUB_REPOSITORY" --name cli-profile --dir "$RUNNER_TEMP/cli-profile"; then
mv "$RUNNER_TEMP/cli-profile/cli.profdata.gz" "/tmp/scriptc-release-assets/scriptc-$VERSION-cli.profdata.gz"
else
echo "Run $RUN has no cli-profile artifact"
fi
- name: Create GitHub Release
run: |
VERSION="${{ needs.check-release.outputs.version }}"
+2
View File
@@ -9,6 +9,7 @@ node_modules/
!tests/corpus/*/node_modules/
!tests/fixtures/npm-static/node_modules/
!tests/coverage-fixtures/wasm/node_modules/
!tests/coverage-fixtures/node-islands*/node_modules/
!tests/coverage-fixtures/dynamic-tier/package/node_modules/
!tests/fixtures/npm-static/nested/inner/node_modules/
!tests/fixtures/npm-static/node_modules/*/node_modules/
@@ -33,6 +34,7 @@ output/
/packages/runtime-*/runtime-pack.json
/packages/runtime-*/.runtime-pack-*
!tests/fixtures/npm/node_modules/*/dist/
!tests/fixtures/npm/node_modules/@*/*/dist/
!tests/fixtures/npm/workspace/*/dist/
!tests/fixtures/npm/cases/workspace-copied/members/*/dist/
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
+45 -2
View File
@@ -6,6 +6,51 @@ All notable changes to scriptc will be documented in this file.
<!-- release:start -->
## 0.4.4
### Highlights
- **Fixes for 0.4.3:**
- Unchanged rebuilds hit the frontend cache again. An unchanged rebuild of a 23-module program takes 93 ms instead of 1.26 s, and of tsc-ts (dev) 1.6 s instead of 58.2 s.
- The native compiler builds programs with several typed-array accesses at one masked index in a loop again, including tsc-ts's release build, which failed with SC3004.
- `x!` returned from a function, used as an arrow function's result, or passed as an argument keeps null or undefined when the destination type holds it, as JavaScript does. `x!` operands of `+`, `+=` and other conversions that hold undefined or null convert as Node does.
- Effect 4 programs whose generators yield clients built by other generators run again.
- The cache for untyped property access in programs that declare classes is reverted, fixing a crash in Effect 4 observability programs.
- **Faster:**
- Number compares and typed-array indexes use the exact integer of sums like `i + 1`: a heap sift-down over an `Int32Array` runs 17.5% faster.
- glibc's per-thread heaps use huge pages: the VS Code type-check runs 3.4% faster.
- Mid-size executables (over 1 MiB of LLVM text) build in at least eight partitions: the LLVM backend phase takes 34 to 74% less time for three.js programs of 1.4 to 7.8 MB.
- JavaScript class instances are boxed at their exact class, and stores into nested string-keyed dictionaries keep class method calls direct.
- `--target-cpu` selects an x86-64 microarchitecture level.
- **Node.js parity:**
- X25519 keys and `crypto.diffieHellman`; `existsSync` and `fs.promises` accept `file:` URLs; `Buffer.from` ranges over ArrayBuffers, the deprecated `Buffer()` constructor and `Buffer` as a value.
- Dates are objects with identity; `Promise.all` over different entry types settles on Node's turns; completion callbacks run in their call's AsyncLocalStorage context.
- TypedArray methods run on typed arrays held as untyped values, and unions of typed-array classes compile. Borrowed `TypedArray.prototype.set` calls compile and throw Node's TypeError for other targets.
- Split CommonJS `require` bindings, `__`-prefixed exports, string-key destructuring of `any` values, and record key order for values JavaScript built.
- **npm packages:** zod 4 and `@hono/zod-validator` values are projected at the declaration boundary.
<!-- release:end -->
## 0.4.3
### Highlights
- **Faster checking and smaller builds with tsc-ts 0.0.9:** scriptc checks with tsc-ts 0.0.9, and builds reuse the bundled declaration files' diagnostics instead of checking them again. Small IR builds are 10.6 to 16.0% faster and small executable builds 5.5 to 7.6% faster, with identical output. A native build of tsc-ts with this release checks 0.4 to 6.6% faster than with 0.4.2 and peaks 4 to 16% lower.
- **Integer arrays and loops:**
- Number arrays that only hold int32 values use 4-byte slots in locals, across direct calls and in module globals, and loop-carried locals written from their elements stay int32. A workload that walks arrays of cursors runs 54.5% faster.
- Loops with two counters closing in on each other (`for (let i = lo, j = hi; i < j; i++, j--)`) and element copies through `a[i]!` run without per-element checks, and two-counter `while` loops run on integer counters.
- Loops over typed arrays and charAt scans are versioned on their bounds, and ASCII strings are scanned as bytes.
- **Less work for untyped and dynamic code:**
- String-keyed record reads under `??` and `!` no longer allocate: a dictionary-walking workload makes no union boxes instead of 363,359 and runs 12 to 15% faster.
- Checked-dynamic method calls dispatch on method ids, observed class methods call natively while their slot is unchanged, and calls on bag-tracked class instances skip building their views.
- **Node.js parity:**
- `crypto.getCipherInfo`, AES-CCM, AES key wrap, the legacy `createCipheriv` modes, `checkPrime`/`generatePrime`, and Node's argument checks for `generateKeyPair`.
- Streams: `construct`, `writev`, `decodeStrings` and `defaultEncoding`, including `writev` on `Transform` and `PassThrough`.
- `FinalizationRegistry`, `new.target`, `String.prototype.at` on untyped values, `Error.stackTraceLimit`, `spawnSync`'s `env` option, and class constructors called through `call`/`apply` throw Node's TypeError.
- Records enumerate in the key order their object literal wrote, and objects converting to a union with an empty-record arm keep every field.
- **npm packages:** zod 3 compiles natively and zod 4's core runs, minified packages such as marked compile, chalk 5 and hot-shots 8 build from built-in shims, and `@hono/node-server` 2.x is built in. User code types npm package declarations at a checked, identity-preserving boundary.
- **Builds:** the cached build is reused after whitespace and comment edits, function bodies are emitted on forked workers, and the LLVM emitter reads its analyses from one recorded walk of each module.
## 0.4.2
### Highlights
@@ -159,8 +204,6 @@ All notable changes to scriptc will be documented in this file.
- **The same program compiles to the same executable wherever it sits on disk** and whatever order its functions lower in, and edits outside a declaration do not rename the helpers and closures inside it.
- **Libraries that use untyped byte arrays** link.
<!-- release:end -->
## 0.4.1
### Highlights
+1 -1
View File
@@ -16,7 +16,7 @@ CI (`.github/workflows/release.yml`) compares the version in `packages/cli/packa
The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI bundles its TypeScript checker, LLVM helper, runtime pack, declarations, and comptime evaluator. npm postinstall installs the matching native executable; Node is needed for npm installation but not native compilation or execution. The user's toolchain performs the final platform link. Sanitized builds also use it to instrument program LLVM and compile the C runtime. Platform packages publish ahead of `@scriptc/compiler` and `scriptc`. The GitHub release attaches standalone compiler archives, their SHA-256 checksums, and the surface manifest after npm publication succeeds.
Release builds of the native CLI optimize the compiler with the committed profile `packages/compiler/native/cli.profdata` on every platform. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. Refresh the profile on a quiet Linux x64 host when compiler code has changed substantially since the last refresh: `node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout>` builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Commit the new profile on its own.
Release builds of the native CLI optimize the compiler with a profile collected on the release commit. The `cli-profile` job runs `node scripts/refresh-cli-profile.mjs` on Linux x64, which builds an instrumented compiler, runs it on tsc-ts, a slice of the corpus, and the runtime benchmark workloads, and writes the merged sparse profile. Every platform's CLI build uses that profile (`SCRIPTC_NATIVE_PROFILE_USE`), and the GitHub release attaches it as `scriptc-<version>-cli.profdata.gz`. Code the profile does not describe, or describes from older compiler sources, is optimized as without a profile. When the profile job fails, the CLI builds use the committed profile `packages/compiler/native/cli.profdata.gz`, which local and CI builds of the native compiler also use. Refresh the committed profile when compiler code has changed substantially since the last refresh: run `node scripts/refresh-cli-profile.mjs --tsc-ts <tsc-ts checkout>` on a quiet Linux x64 host and commit the new profile on its own.
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret.
Each published package must have a GitHub Actions trusted publisher for
+7
View File
@@ -10,12 +10,17 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
| `date-math` | cpu | schedules built with Date setters (local and UTC), weekday and month-end rules through the getters, Dates sorted by time value |
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass |
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` |
| `line-reader` | cpu | `readline` over a file stream: 'line' events for a 67 MB access log (every 50th line not ASCII), with a status count, a path tally and byte totals |
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes |
@@ -27,10 +32,12 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
| `template-render` | cpu | template literals, escaping, `map`/`join` |
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
| `union-node-sets` | cpu | syntax-tree passes over a discriminated union of object literals: a `Set` of visited nodes, a `Map` from node to value, conversions to a sub-union of leaves |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
@@ -0,0 +1,6 @@
{
"entry": "workloads/date-math.ts",
"args": [
"40"
]
}
+52
View File
@@ -0,0 +1,52 @@
// Calendar arithmetic on Dates: schedules built by adjusting Dates with the
// UTC and local setters, weekday and month-end rules read back through the
// getters, Dates compared and sorted by time value, and ISO text for a
// sample. Argument: the number of rounds.
process.env.TZ = "America/New_York";
const rounds = Number(process.argv[2] ?? "40");
let seed = 2026;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
/** The next business day at 09:00 local time on or after `from`. */
function nextBusinessDay(from: Date): Date {
const day = new Date(from.getTime());
day.setHours(9, 0, 0, 0);
if (day < from) day.setDate(day.getDate() + 1);
while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1);
return day;
}
/** The last day of `date`'s month, in UTC. */
function monthEnd(date: Date): number {
const end = new Date(date.getTime());
end.setUTCDate(1);
end.setUTCMonth(end.getUTCMonth() + 1);
end.setUTCDate(0);
return end.getUTCDate();
}
let checksum = 0;
let sample = "";
for (let round = 0; round < rounds; round++) {
const due: Date[] = [];
const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12));
for (let i = 0; i < 4000; i++) {
cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000));
const next = nextBusinessDay(cursor);
checksum += next.getDate() + monthEnd(next) + next.getHours();
if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15);
due.push(next);
}
due.sort((a, b) => +a - +b);
let previous = due[0]!;
for (const date of due) {
checksum += (+date - +previous) / 3600000;
previous = date;
}
sample = due[due.length >> 1]!.toISOString();
}
console.log(Math.round(checksum), sample);
@@ -0,0 +1,6 @@
{
"entry": "workloads/dynamic-properties.mjs",
"args": [
"1000000"
]
}
@@ -0,0 +1,56 @@
// Untyped JavaScript reading and writing properties of values it never
// declares types for: records built by object literals in two key orders,
// nested records, property updates, and instances of a class and its
// subclass read and written the same way. Argument: the number of
// iterations.
const rounds = Number(process.argv[2] ?? "1000000");
class Cursor {
constructor(x, y) {
this.x = x;
this.y = y;
}
step(dx, dy) {
this.x += dx;
this.y += dy;
return this;
}
}
class Marker extends Cursor {
constructor(x, y, label) {
super(x, y);
this.label = label;
}
}
function makeItem(i) {
if (i % 3 === 0)
return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } };
return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } };
}
function cost(item) {
return item.price * item.qty + item.meta.weight;
}
function restock(item, round) {
item.qty = (item.qty + round) % 7;
item.meta.tag = (item.meta.tag + 1) % 7;
}
function distance(a, b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
const items = [];
for (let i = 0; i < 1024; i++) items.push(makeItem(i));
const cursors = [];
for (let i = 0; i < 64; i++)
cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i));
const origin = { x: 0, y: 0 };
let checksum = 0;
for (let r = 0; r < rounds; r++) {
const item = items[r & 1023];
checksum += cost(item);
restock(item, r & 3);
checksum += item.meta.tag;
const cursor = cursors[r & 63];
checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin);
if (cursor.x > 100 || cursor.x < -100) cursor.x = 0;
if (cursor.y > 100 || cursor.y < -100) cursor.y = 0;
}
console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label);
@@ -0,0 +1,6 @@
{
"entry": "workloads/index-cursors.ts",
"args": [
"20000"
]
}
@@ -0,0 +1,25 @@
// Arrays of indices walked by loop-carried cursors: each step reads the
// next index from the array and relinks the visited entries, so the
// cursor and every stored value are small integers read back from the
// array itself.
const N = 4096;
function relink(rounds: number): number {
const next: number[] = [];
for (let i = 0; i < N; i++) next.push(i + 1 < N ? i + 1 : 0);
let at = next[0]!;
let total = 0;
for (let r = 0; r < rounds; r++) {
for (let s = 0; s < N; s++) {
const t = next[at]!;
next[at] = next[t]!;
next[t] = at;
at = t;
total = (total + at) | 0;
}
}
return total;
}
const rounds = Number(process.argv[2] ?? "1");
console.log("relink", relink(rounds));
@@ -0,0 +1,6 @@
{
"entry": "workloads/json-unions.ts",
"args": [
"6"
]
}
+155
View File
@@ -0,0 +1,155 @@
// JSON text parsed into discriminated unions: an event log with eight kinds
// of event and a nested expression tree with nine kinds of node, cast with
// `JSON.parse(text) as T`, then aggregated and evaluated. Argument: the
// number of rounds.
type Event =
| { kind: "click"; target: string; x: number; y: number }
| { key: string; kind: "key"; repeat: boolean }
| { kind: "scroll"; offset: number }
| { kind: "resize"; height: number; width: number }
| { kind: "focus"; target: string }
| { kind: "blur"; target: string }
| { kind: "load"; ms: number; url: string }
| { code: number; kind: "error"; message: string };
type Expr =
| { kind: "num"; value: number }
| { kind: "var"; name: string }
| { kind: "neg"; operand: Expr }
| { kind: "add"; left: Expr; right: Expr }
| { kind: "sub"; left: Expr; right: Expr }
| { kind: "mul"; left: Expr; right: Expr }
| { kind: "max"; items: Expr[] }
| { else: Expr; kind: "if"; test: Expr; then: Expr }
| { body: Expr; kind: "let"; name: string; value: Expr };
let seed = 12345;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const targets = ["menu", "search", "button", "list", "footer"];
function eventText(i: number): string {
const target = targets[i % targets.length]!;
switch (i % 8) {
case 0:
return `{"kind":"click","target":"${target}","x":${i % 640},"y":${i % 480}}`;
case 1:
return `{"key":"k${i % 26}","kind":"key","repeat":${i % 3 === 0}}`;
case 2:
return `{"kind":"scroll","offset":${(i * 7) % 2000}}`;
case 3:
return `{"height":${400 + (i % 300)},"kind":"resize","width":${600 + (i % 500)}}`;
case 4:
return `{"kind":"focus","target":"${target}"}`;
case 5:
return `{"kind":"blur","target":"${target}"}`;
case 6:
return `{"kind":"load","ms":${i % 900},"url":"/page/${i % 50}"}`;
default:
return `{"code":${400 + (i % 100)},"kind":"error","message":"failed ${i % 13}"}`;
}
}
const names = ["a", "b", "c", "d"];
function exprText(depth: number): string {
const pick = depth <= 0 ? Math.floor(random() * 2) : Math.floor(random() * 9);
switch (pick) {
case 0:
return `{"kind":"num","value":${Math.floor(random() * 100)}}`;
case 1:
return `{"kind":"var","name":"${names[Math.floor(random() * names.length)]!}"}`;
case 2:
return `{"kind":"neg","operand":${exprText(depth - 1)}}`;
case 3:
case 4:
case 5: {
const kind = pick === 3 ? "add" : pick === 4 ? "sub" : "mul";
return `{"kind":"${kind}","left":${exprText(depth - 1)},"right":${exprText(depth - 1)}}`;
}
case 6:
return `{"items":[${exprText(depth - 1)},${exprText(depth - 2)},${exprText(depth - 2)}],"kind":"max"}`;
case 7:
return `{"else":${exprText(depth - 2)},"kind":"if","test":${exprText(depth - 1)},"then":${exprText(depth - 2)}}`;
default:
return `{"body":${exprText(depth - 1)},"kind":"let","name":"${names[Math.floor(random() * names.length)]!}","value":${exprText(depth - 2)}}`;
}
}
function evaluate(e: Expr, env: Map<string, number>): number {
switch (e.kind) {
case "num":
return e.value;
case "var":
return env.get(e.name) ?? 1;
case "neg":
return -evaluate(e.operand, env);
case "add":
return (evaluate(e.left, env) + evaluate(e.right, env)) % 1000003;
case "sub":
return (evaluate(e.left, env) - evaluate(e.right, env)) % 1000003;
case "mul":
return (evaluate(e.left, env) * evaluate(e.right, env)) % 1000003;
case "max": {
let best = -Infinity;
for (const item of e.items) best = Math.max(best, evaluate(item, env));
return best;
}
case "if":
return evaluate(e.test, env) > 0 ? evaluate(e.then, env) : evaluate(e.else, env);
case "let": {
const inner = new Map(env);
inner.set(e.name, evaluate(e.value, env));
return evaluate(e.body, inner);
}
}
}
const rounds = Number(process.argv[2] ?? "6");
const eventsText = `[${Array.from({ length: 60000 }, (_, i) => eventText(i)).join(",")}]`;
const treesText = `[${Array.from({ length: 400 }, () => exprText(9)).join(",")}]`;
let clicks = 0;
let keys = 0;
let scrolled = 0;
let area = 0;
let loadMs = 0;
let errors = 0;
let focus = 0;
let total = 0;
for (let round = 0; round < rounds; round++) {
const events = JSON.parse(eventsText) as Event[];
for (const event of events) {
switch (event.kind) {
case "click":
clicks += event.x + event.y;
break;
case "key":
if (event.repeat) keys++;
break;
case "scroll":
scrolled += event.offset;
break;
case "resize":
area += (event.width * event.height) % 1000;
break;
case "focus":
case "blur":
focus += event.target.length;
break;
case "load":
loadMs += event.ms;
break;
case "error":
errors += event.code % 7;
break;
}
}
const trees = JSON.parse(treesText) as Expr[];
const env = new Map([
["a", 3],
["b", 5],
]);
for (const tree of trees) total = (total + evaluate(tree, env)) % 1000003;
}
console.log(clicks, keys, scrolled, area, loadMs, errors, focus, total);
@@ -0,0 +1,6 @@
{
"entry": "workloads/keyed-option-writes.ts",
"args": [
"4"
]
}
@@ -0,0 +1,76 @@
// Class methods whose names are also keys of option records written through
// element accesses: `enabled[name] = true` over a union of literal names and
// `totals["count"] = n`. The writes store booleans and numbers into plain
// records, so the methods keep their native calls.
type Step = "scale" | "offset" | "clamp";
const enabled: Record<Step, boolean> = { scale: false, offset: false, clamp: false };
function enable(name: Step): void {
enabled[name] = true;
}
const totals: Record<string, number> = {};
class Transform {
private readonly factor: number;
private readonly shift: number;
constructor(factor: number, shift: number) {
this.factor = factor;
this.shift = shift;
}
scale(x: number): number {
return enabled.scale ? x * this.factor : x;
}
offset(x: number): number {
return enabled.offset ? x + this.shift : x;
}
clamp(x: number): number {
if (!enabled.clamp) return x;
return x < 0 ? 0 : x > 1000 ? 1000 : x;
}
}
class Counter {
private seen = 0;
private sum = 0;
add(value: number): void {
this.seen++;
this.sum += value;
}
count(): number {
return this.seen;
}
mean(): number {
return this.seen === 0 ? 0 : this.sum / this.seen;
}
}
const scale = Number(process.argv[2] ?? "1");
const steps: Step[] = ["scale", "offset", "clamp"];
for (const step of steps) enable(step);
const transforms: Transform[] = [];
for (let i = 0; i < 16; i++) transforms.push(new Transform(1 + (i % 5) * 0.25, (i % 7) - 3));
const counters: Counter[] = [];
for (let i = 0; i < 8; i++) counters.push(new Counter());
let seed = 12345;
let checksum = 0;
const rounds = Math.floor(2_000_000 * scale);
for (let i = 0; i < rounds; i++) {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
const t = transforms[seed & 15]!;
const value = t.clamp(t.offset(t.scale((seed >>> 8) % 1200)));
const counter = counters[(seed >>> 4) & 7]!;
counter.add(value);
checksum += value;
if ((i & 65535) === 0) totals["count"] = counter.count();
}
let mean = 0;
let seen = 0;
for (const counter of counters) {
mean += counter.mean();
seen += counter.count();
}
console.log("seen", seen, "count", totals["count"], "mean", (mean / counters.length).toFixed(4), "checksum", checksum);
@@ -0,0 +1,7 @@
{
"entry": "workloads/line-reader.ts",
"input": "line-reader",
"args": [
"{input}"
]
}
@@ -0,0 +1,34 @@
// Line-by-line processing of a log file with readline over a file stream:
// 'line' events for about 67 MB of access-log lines, with light work per
// line (a status count, a path tally, byte totals).
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
const path = process.argv[2]!;
const statuses = new Map<string, number>();
const paths = new Map<string, number>();
let lines = 0;
let chars = 0;
let bytes = 0;
let longest = 0;
const input = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
input.on("line", (line: string) => {
lines++;
chars += line.length;
if (line.length > longest) longest = line.length;
const quote = line.indexOf('"');
const space = line.indexOf(" ", quote);
const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1));
paths.set(path, (paths.get(path) ?? 0) + 1);
const close = line.indexOf('"', space);
const status = line.slice(close + 2, close + 5);
statuses.set(status, (statuses.get(status) ?? 0) + 1);
bytes += Number(line.slice(close + 6, line.indexOf(" ", close + 6)));
});
input.on("close", () => {
console.log(`${lines} lines, ${chars} characters, longest ${longest}`);
console.log(`bytes ${bytes}`);
for (const status of [...statuses.keys()].sort()) console.log(`status ${status}: ${statuses.get(status)}`);
for (const prefix of [...paths.keys()].sort()) console.log(`path ${prefix}: ${paths.get(prefix)}`);
});
@@ -0,0 +1,6 @@
{
"entry": "workloads/throw-chains.ts",
"args": [
"1"
]
}
@@ -0,0 +1,196 @@
// Exceptions that cross many frames: throws from helpers below a recursion,
// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a
// callback), with each frame holding an array across its call, and a
// backtracking parser that uses exceptions for control flow.
class ParseError extends Error {
readonly at: number;
constructor(at: number, message: string) {
super(message);
this.at = at;
}
}
/** A thrown value that is not an Error (no stack to capture). */
class Stop {
readonly code: number;
constructor(code: number) {
this.code = code;
}
}
const held: number[][] = [];
function fail(n: number, plain: boolean): number {
if (plain) throw new Stop(n);
throw new RangeError("chain " + n);
}
function check(n: number, plain: boolean): number {
return (n & 3) === 0 ? fail(n, plain) : n;
}
function validate(n: number, plain: boolean): number {
return check(n, plain) + (n & 1);
}
function bottom(n: number, plain: boolean): number {
const box = [n, -1];
const r = validate(n, plain);
held[0] = box;
return r + box.length;
}
function walk(n: number, depth: number, plain: boolean): number {
const box = [n, depth];
const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain);
held[depth] = box;
return r + box.length;
}
function chains(rounds: number): string {
const out: string[] = [];
for (const depth of [2, 5, 20])
for (const plain of [false, true]) {
let caught = 0;
let sum = 0;
for (let i = 0; i < rounds; i++) {
try {
sum += walk(i, depth - 2, plain);
} catch (error) {
caught++;
sum += error instanceof Stop ? error.code & 7 : 1;
}
}
out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`);
}
return out.join("\n");
}
// The same chains through a callback: each level visits the next with a
// closure, as tree visitors do.
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
const box = [n, depth];
const r = depth === 0 ? bottom(n, false) : each(n, depth - 1);
held[depth] = box;
return r + box.length;
}
function visits(rounds: number): string {
const out: string[] = [];
const each = (n: number, depth: number): number => visit(n, depth, each);
for (const depth of [2, 5, 20]) {
let caught = 0;
let sum = 0;
for (let i = 0; i < rounds; i++) {
try {
sum += visit(i, depth - 2, each);
} catch (error) {
caught++;
sum += error instanceof RangeError ? 1 : 0;
}
}
out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`);
}
return out.join("\n");
}
// expr := term (("+" | "-") term)*; term := atom ("*" atom)*;
// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name.
// A call and a name start alike: the parser tries the call first and
// backtracks when it fails.
let text = "";
let pos = 0;
function peek(): string {
return pos < text.length ? text[pos]! : "";
}
function expect(c: string): void {
if (peek() !== c) throw new ParseError(pos, `expected ${c}`);
pos++;
}
function name(): string {
const start = pos;
while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++;
if (pos === start) throw new ParseError(pos, "expected a name");
return text.slice(start, pos);
}
function call(): number {
const callee = name();
expect("(");
let value = expr() + callee.length;
while (peek() === ",") {
pos++;
value += expr();
}
expect(")");
return value;
}
function atom(): number {
const c = peek();
if (c >= "0" && c <= "9") {
let value = 0;
while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48);
return value;
}
if (c === "(") {
pos++;
const value = expr();
expect(")");
return value;
}
const start = pos;
try {
return call();
} catch (error) {
if (!(error instanceof ParseError)) throw error;
pos = start;
return name().length;
}
}
function term(): number {
let value = atom();
while (peek() === "*") {
pos++;
value = (value * atom()) % 1000003;
}
return value;
}
function expr(): number {
let value = term();
while (peek() === "+" || peek() === "-") {
const op = text[pos++];
value = op === "+" ? value + term() : value - term();
}
return value;
}
let seed = 4242;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
function source(depth: number): string {
const r = random();
if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100));
if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8));
if (r < 0.6) return `(${source(depth + 1)})`;
if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`;
return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`;
}
function parse(count: number): string {
let total = 0;
let failed = 0;
for (let i = 0; i < count; i++) {
// Every tenth input lacks its last character, which fails deep inside it.
const full = source(0);
text = i % 10 === 0 ? full.slice(0, -1) : full;
pos = 0;
try {
total = (total + expr()) % 1000003;
if (pos !== text.length) throw new ParseError(pos, "trailing input");
} catch (error) {
if (!(error instanceof ParseError)) throw error;
failed++;
}
}
return `parsed ${count}: total ${total} failed ${failed}`;
}
const scale = Number(process.argv[2] ?? "1");
console.log(chains(Math.floor(60000 * scale)));
console.log(visits(Math.floor(60000 * scale)));
console.log(parse(Math.floor(40000 * scale)));
@@ -0,0 +1,6 @@
{
"entry": "workloads/union-node-sets.ts",
"args": [
"1"
]
}
@@ -0,0 +1,141 @@
// Syntax-tree passes over a discriminated union of object literals, the way
// compilers walk their IR: a DAG of shared subexpressions, a `Set` of
// visited nodes, a `Map` from node to its value, and conversions between
// the whole union and the sub-unions passes narrow it to.
type Num = { kind: "num"; value: number };
type Var = { kind: "var"; slot: number };
type Neg = { kind: "neg"; operand: Node };
type Add = { kind: "add"; left: Node; right: Node };
type Mul = { kind: "mul"; left: Node; right: Node };
type Pick = { kind: "pick"; cond: Node; then: Node; else_: Node };
type Leaf = Num | Var;
type Node = Leaf | Neg | Add | Mul | Pick;
function children(node: Node, visit: (child: Node) => void): void {
switch (node.kind) {
case "num":
case "var":
return;
case "neg":
visit(node.operand);
return;
case "add":
case "mul":
visit(node.left);
visit(node.right);
return;
case "pick":
visit(node.cond);
visit(node.then);
visit(node.else_);
return;
}
}
function leafValue(leaf: Leaf, env: number[]): number {
return leaf.kind === "num" ? leaf.value : env[leaf.slot]!;
}
function evaluate(node: Node, env: number[], memo: Map<Node, number>): number {
const cached = memo.get(node);
if (cached !== undefined) return cached;
let result: number;
switch (node.kind) {
case "num":
case "var":
result = leafValue(node, env);
break;
case "neg":
result = -evaluate(node.operand, env, memo);
break;
case "add":
result = (evaluate(node.left, env, memo) + evaluate(node.right, env, memo)) % 1000003;
break;
case "mul":
result = (evaluate(node.left, env, memo) * evaluate(node.right, env, memo)) % 1000003;
break;
case "pick":
result =
evaluate(node.cond, env, memo) % 2 === 0
? evaluate(node.then, env, memo)
: evaluate(node.else_, env, memo);
break;
}
memo.set(node, result);
return result;
}
function reachable(root: Node): number {
const seen = new Set<Node>();
const stack: Node[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
if (seen.has(node)) continue;
seen.add(node);
children(node, (child) => {
if (!seen.has(child)) stack.push(child);
});
}
return seen.size;
}
function leaves(root: Node): Leaf[] {
const out: Leaf[] = [];
const seen = new Set<Node>();
const walk = (node: Node): void => {
if (seen.has(node)) return;
seen.add(node);
if (node.kind === "num" || node.kind === "var") out.push(node);
else children(node, walk);
};
walk(root);
return out;
}
let seed = 2463534242;
function next(): number {
seed ^= seed << 13;
seed ^= seed >>> 17;
seed ^= seed << 5;
return seed >>> 0;
}
function build(size: number): Node {
const pool: Node[] = [];
for (let i = 0; i < 64; i++)
pool.push(i % 2 === 0 ? { kind: "num", value: i * 7 + 1 } : { kind: "var", slot: i % 16 });
// Operands come from the most recent nodes, so most of the DAG is
// reachable from the root and many nodes are shared.
const recent = (): Node => pool[pool.length - 1 - (next() % 48)]!;
for (let i = 0; i < size; i++) {
const a = recent();
const b = recent();
const r = next() % 10;
let node: Node;
if (r < 1) node = { kind: "neg", operand: a };
else if (r < 5) node = { kind: "add", left: a, right: b };
else if (r < 8) node = { kind: "mul", left: a, right: b };
else node = { kind: "pick", cond: a, then: b, else_: recent() };
pool.push(node);
}
return pool[pool.length - 1]!;
}
const scale = Number(process.argv[2] ?? "1");
const rounds = Math.max(1, Math.round(40 * scale));
let checksum = 0;
let nodes = 0;
let leafCount = 0;
for (let round = 0; round < rounds; round++) {
const root = build(20_000);
nodes += reachable(root);
const found = leaves(root);
leafCount += found.length;
for (let pass = 0; pass < 4; pass++) {
const env: number[] = [];
for (let i = 0; i < 16; i++) env.push((round * 31 + pass * 7 + i) % 97);
checksum = (checksum + evaluate(root, env, new Map())) % 1000003;
for (const leaf of found) checksum = (checksum + leafValue(leaf, env)) % 1000003;
}
}
console.log("rounds", rounds, "nodes", nodes, "leaves", leafCount, "checksum", checksum);
+43
View File
@@ -135,6 +135,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dt><code>--detached-exit</code></dt>
<dd>On Linux, when the program calls <code>process.exit()</code>, hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.</dd>
<dt><code>--target-cpu &lt;baseline|x86-64-v2|x86-64-v3|x86-64-v4|native&gt;</code></dt>
<dd>On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default, <code>baseline</code>, runs on any x86-64 CPU. A higher level lets LLVM use more instructions, and the executable then runs only on CPUs that support that level. <code>native</code> uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See <a href="#target-cpu">Target CPU</a>.</dd>
<dt><code>--profile-generate &lt;pattern&gt;</code></dt>
<dd>On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where <code>%p</code> expands to the process ID; the <code>LLVM_PROFILE_FILE</code> environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
@@ -207,6 +210,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dt><code>SCRIPTC_CHECKER</code></dt>
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code.</dd>
<dt><code>SCRIPTC_CHECKER_SERVER</code></dt>
<dd>Set to <code>1</code> on Linux and macOS to check with a tsc-ts server that later builds share. It takes effect with a tsc-ts release that serves <code>tsc-ts --api --listen</code>. The tsc-ts 0.0.9 that this version of scriptc includes does not, so each build checks with its own tsc-ts process, as without the variable. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (<code>TSC_TS_LISTEN_MEMORY</code> sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.</dd>
</dl>
```console
@@ -228,6 +234,43 @@ $ scriptc build app.ts --optimization=speed --profile-use app.profdata
Train on workloads like the ones the program will run: code the training never reached is optimized as without a profile. The profile can be committed and reused as the program changes. Functions whose control flow changed since the profile was collected lose their profile, and the rest keep theirs, so regenerate it when much of the program has changed. [Build parallelism](/docs/how-it-works#build-parallelism) describes what a profile adds to build time.
## Target CPU
By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it:
<table>
<thead>
<tr>
<th>Level</th>
<th>Adds</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>x86-64-v2</code></td>
<td>SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT</td>
</tr>
<tr>
<td><code>x86-64-v3</code></td>
<td>AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE</td>
</tr>
<tr>
<td><code>x86-64-v4</code></td>
<td>AVX-512 (F, BW, CD, DQ, VL)</td>
</tr>
</tbody>
</table>
```console
$ scriptc build app.ts --target-cpu=x86-64-v3 -o app
```
Loops over typed arrays and numbers gain the most, because LLVM vectorizes them with wider registers. Some loops run slower at a higher level, so measure the program on the machines it will run on.
An executable built for a level runs only on CPUs that support that level. On an older CPU, it crashes with an illegal-instruction signal (`SIGILL`) when it reaches an instruction the CPU lacks. A named level is portable to every machine of that level. `native` selects the highest level the build machine supports, so use it only for executables that run on the machine that built them or on identical machines.
The level applies to the program's own code. The runtime pack keeps its baseline code. Builds for different levels never share cached artifacts, and `baseline` builds are the same as builds without the option.
## LLVM backend
scriptc generates LLVM IR and compiles it to native code. Executable builds link the result with the matching precompiled runtime pack. Use <code>--emit=llvm</code> to inspect the generated code.
+1 -1
View File
@@ -245,7 +245,7 @@ $ scriptc build scale.ts
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).
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of a callback are followed only when scriptc copies it into the loop that runs the array method: a callback written at a `map` or `filter` call whose captured variables are not reassigned, such as `xs.map((x) => (x * 2) | 0)`. Other callback results are not followed, so for example an array built by `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
+8 -1
View File
@@ -53,7 +53,7 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ
scriptc compiles a package automatically when it:
- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and
- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names.
- ships JavaScript that isn't a webpack bundle. Minified JavaScript compiles like unminified JavaScript.
The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use.
@@ -92,6 +92,13 @@ scriptc implements a few packages itself and compiles that implementation in pla
- graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying.
- An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there.
`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies stream to the handler as it reads them, and a body the handler leaves unread is drained after the response, up to 64 MiB or 500 ms, as the package does. Known differences:
- Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files.
- The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`.
- The global `Request` and `Response` stay the built-in classes, so `overrideGlobalObjects` has no effect, and a `Response` reads its default text content type as `text/plain;charset=UTF-8` where the package's replacement reads `text/plain; charset=UTF-8`. Written responses carry the package's spelling.
- A streamed response is written without waiting for `drain`, and a failed handler answers 504 only for an error whose `name` is `TimeoutError`.
Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled.
<table>
+6 -7
View File
@@ -42,28 +42,27 @@ Parsing, type checking, lowering, and LLVM IR generation run one after another o
### Partitions
The LLVM helper divides an executable's LLVM IR into partitions by its size: one partition per 4 MiB of LLVM text, rounded up to a power of two, and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets.
The LLVM helper divides an executable's LLVM IR into partitions by its size. Up to 1 MiB of LLVM text is one partition, and up to 2 MiB is four. Larger programs get one partition per 4 MiB of LLVM text, rounded up to a power of two, and at least 8 and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets.
<table>
<thead>
<tr><th>LLVM text</th><th>Partitions</th></tr>
</thead>
<tbody>
<tr><td>Up to 4 MiB</td><td>1</td></tr>
<tr><td>Over 4 MiB, up to 8 MiB</td><td>2</td></tr>
<tr><td>Over 8 MiB, up to 16 MiB</td><td>4</td></tr>
<tr><td>Over 16 MiB, up to 32 MiB</td><td>8</td></tr>
<tr><td>Up to 1 MiB</td><td>1</td></tr>
<tr><td>Over 1 MiB, up to 2 MiB</td><td>4</td></tr>
<tr><td>Over 2 MiB, up to 32 MiB</td><td>8</td></tr>
<tr><td>Over 32 MiB, up to 64 MiB</td><td>16</td></tr>
<tr><td>Over 64 MiB, up to 128 MiB</td><td>32</td></tr>
<tr><td>Over 128 MiB</td><td>64</td></tr>
</tbody>
</table>
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. A function's partition follows from its name, and a small internal function goes with the one function that calls it, so an edit leaves the other functions in their partitions.
The count depends only on the size of the LLVM text, so a program has the same partitions, and builds the same executable, on every host. In programs with 32 partitions or more, functions from the same source file are placed in the same partition, so that their calls to each other stay inside it; a file larger than 1 MiB of LLVM text or than an average partition is divided into parts by function name. Other functions, and the functions of smaller programs, are placed by their name, and a small internal function goes with the one function that calls it. An edit leaves almost every other function in its partition.
These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler.
The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with two partitions uses at most two cores in the backend. A program of one partition is optimized and compiled on one thread.
The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with four partitions uses at most four cores in the backend. A program of one partition is optimized and compiled on one thread.
How partitions are compiled depends on `--optimization`:
+8
View File
@@ -220,6 +220,14 @@ title: "Limitations"
<include>../limitations/tls-ciphers.mdx</include>
### TLS certificates
<include>../limitations/tls-certificates.mdx</include>
### TLS sessions
<include>../limitations/tls-sessions.mdx</include>
### Console and process streams
<include>../limitations/console-and-process-streams.mdx</include>
@@ -1,3 +1,8 @@
`Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects.
Encoding arguments must be supported literals.
`Buffer.from(arrayBuffer, byteOffset, length)` is a view of the ArrayBuffer or SharedArrayBuffer, with Node's offset and length checks.
Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error.
The deprecated `Buffer()` and `new Buffer()` take up to three arguments, without spreads, and follow Node: the first call made outside `node_modules` reports DEP0005 on the next tick, a number allocates zero-filled bytes, and other arguments convert as `Buffer.from` does.
In JavaScript, `Buffer` read as a value (the global or the buffer module's `Buffer`) is one function with Node's statics and `poolSize`.
Calling or constructing it through a stored value, reading its `prototype`, and its `copyBytesFrom` and `of` statics throw an unsupported-operation error.
`Buffer.prototype` members read as values are functions that run on Buffer receivers; inherited and symbol-keyed members throw an unsupported-operation error.
@@ -3,6 +3,8 @@
When `strictEqual` compares function values, `actual` and `expected` read as `undefined`; when `assert.throws` or `assert.rejects` reports an error that is not an instance of the expected class, or that does not match an expected object, `expected` reads as `undefined`.
Values whose type can be cyclic, such as recursive records, also read as `undefined`.
`assert.AssertionError` used as a value outside `instanceof` throws `SC2020`.
The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument.
`throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`.
`assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused.
`assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals.
Checked-dynamic values compared deeply have no cycle detection.
@@ -1,2 +1,12 @@
- **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node.
Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started.
The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node.
Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with.
A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call.
Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call.
Stream events and callbacks run with the stores of the code that triggers them, as in Node.
Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call.
`fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them.
Events of `child_process` children, their standard streams and their IPC channels see the context of the `spawn()` or `fork()` call; stdin's `'finish'` and `send()` callbacks see the context of their call.
A client `http2` session's events see the context of its `http2.connect()` call, and a client stream's events the context of its `request()` call.
An `http2` server's `'session'` and `'stream'` events and the compatibility API's request handler see the context of its `listen()` call; a server stream's own events, and its compatibility request's and response's, run with no store, as in Node.
`AsyncResource` has no lowering yet.
@@ -4,4 +4,5 @@
The `Deflate`, `Inflate`, `Gzip`, `Gunzip`, `DeflateRaw`, `InflateRaw`, and `Unzip` streams, from `createGzip()` and the other factories or `new zlib.Gzip()`, accept Node's options and support writes, piping, `pipeline`, async iteration, `flush`, `params`, `close`, `reset`, and `bytesWritten`.
Each output chunk is its own Buffer rather than a slice of a shared chunk-size buffer.
`zlib.constants` members and their module-level copies read as Node's values.
`util.inspect`, `instanceof zlib.Gzip`, and Brotli and Zstd compression are unsupported.
The Brotli one-shots `brotliCompress` and `brotliDecompress` and their `Sync` forms, and the `BrotliCompress` and `BrotliDecompress` streams, accept Node's options, including `params` and `dictionary`, and produce the same bytes as Node.
`util.inspect` and Zstd compression are unsupported.
@@ -8,7 +8,10 @@
`randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`.
Scrypt supports its default parameters, numeric options, aliases, and memory limit.
Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers.
`createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused.
`createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, CCM, ECB, CFB, CFB8, CFB1, and OFB modes and as key wrap (the wrap and wrap-pad ciphers and their -inv forms), des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused.
`getCipherInfo` answers Node's info for every cipher OpenSSL names, including the ones without a native implementation.
`getCipherInfo` options whose class defines `keyLength` or `ivLength` throw a coded SC2020 error.
The result is Node's object; the SIV ciphers and the SM4 GCM, CCM, and XTS ciphers have no `nid`, and reading it where the declared `CipherInfo` type gives `number` throws a TypeError.
Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`.
Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error.
Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length.
@@ -24,18 +27,26 @@
A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error.
`Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused.
After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own.
`crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available.
`crypto.diffieHellman` computes ECDH and X25519 secrets from EC and X25519 keys, given as KeyObjects or in any form `createPrivateKey` and `createPublicKey` read, with Node's errors; DH keys have no KeyObjects here, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available.
`checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
RSA, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
`generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback.
RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points.
Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error.
They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`.
`crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm.
Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export.
X509Certificate supports `publicKey`, `verify()`, `checkIssued()`, `checkPrivateKey()`, `checkHost()`, `checkEmail()`, `checkIP()`, `toLegacyObject()`, `toString()`, and `toJSON()`, and `String()`, template literals, and `+` give its PEM text as `toString()` does.
`publicKey`, `verify()`, and `checkPrivateKey()` are refused in wasm32-wasi builds, and an Ed25519 certificate's `verify()` needs the `ed25519` literal like the other Ed25519 operations.
Own-key walks over a certificate (`Object.keys()`, `Object.entries()`, spread, `for...in`, and `hasOwnProperty`) are refused, since Node keeps its fields as prototype getters.
`JSON.stringify()` and `console.log()` of a certificate, a certificate stored in an `unknown` or `any` slot, and its methods on a union-typed value are also refused.
Sign and Verify values accumulate `update()` data and are not writable streams.
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
`publicEncrypt`/`privateDecrypt` remain unsupported.
The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript.
@@ -1,5 +1,7 @@
- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported.
Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, 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 `===`.
- **Date:** Spread constructor arguments and locale formatters are unsupported.
Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them.
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint.
A Date converted to `unknown` is the same Date object.
See [Dates and timezones](#dates-and-timezones) for timezone data.
@@ -1,3 +1,3 @@
- **Filesystem URL paths:** Synchronous `fs` functions accept `file:` URL paths and convert them as `fileURLToPath` does.
`existsSync` and the `fs.promises` functions refuse URL paths; pass `fileURLToPath(url)` instead.
A URL path after a string path, or with options that are not literals, is refused, because Node checks those arguments before it converts the URL.
- **Filesystem URL paths:** `fs` and `fs.promises` functions accept `file:` URL paths and convert them as `fileURLToPath` does; `existsSync` answers `false` and the `fs.promises` functions reject for a URL that does not convert.
A URL path with options that are not literals is refused where Node checks those options before it converts the URL, except a `readFileSync` encoding.
`fs.promises` functions also refuse a string path before a URL path; pass `fileURLToPath(url)` instead.
@@ -1,2 +1,3 @@
- **Intl.Segmenter:** Default Unicode grapheme segmentation, iteration, and `containing()` compile.
Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language[-script][-region] written as a string literal (grapheme clusters do not depend on the locale).
In JavaScript dependencies, word and sentence segmenters construct, and their `segment()` throws an SC2020 error.
Locale negotiation over several locales, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
@@ -4,7 +4,8 @@
Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class.
A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
`Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`.
`Object.freeze`, `Object.seal`, and `Object.preventExtensions` of a class instance held in an untyped value change the instance itself, so writes through its typed references are checked the same way.
The `TypeError` for a write to a read-only property that was added to a class instance through an untyped value names `#<Object>` where Node names the class.
Functions support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible`; the operations apply to the function's own properties (`name`, `length`, `prototype`, and the ones a program assigns).
Class constructors are not supported.
A write to a read-only property of a checked dictionary or a function throws a `TypeError` whose message omits Node's `'x' of object '#<Object>'` (or `of function '…'`) suffix.
@@ -1,8 +1,14 @@
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`.
The inherited methods and accessors run with the inheriting object as `this`.
Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class.
`C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons.
Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported.
Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
`instanceof Object` and `instanceof Function` on untyped values follow the prototype chain as in Node, so an object built with `Object.create(null)` is not an `Object`.
@@ -3,7 +3,8 @@
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does.
Later `pipeline` stages and `for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
`Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
@@ -14,11 +15,11 @@
`node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument.
A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
The source may also be an array, a string, a `Set`, a `Map`, a generator or async generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
Stages may take the `{ signal }` argument.
With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
`@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error.
In typed code, a pipeline callback with an async function or async generator destination receives `null` for no error when its error parameter cannot hold `undefined` (@types/node declares it `NodeJS.ErrnoException | null`), and `undefined` when it can, as in Node.
`Readable.from` accepts `objectMode` and `highWaterMark` options.
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options.
Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream.
@@ -27,14 +28,16 @@
A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with.
`stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it.
A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`.
A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported.
`new TransformStream()` without arguments builds the identity transform: its readable side is a web `ReadableStream`, its writable side supports `getWriter`, `abort`, `close`, and `locked`, and the writer supports `write`, `close`, `abort`, `releaseLock`, `ready`, `closed`, and `desiredSize`, with Node's backpressure (a write completes when the readable side reads its chunk).
A web stream in a later compose stage, `Readable.toWeb`, a `TransformStream` with a transformer or queuing strategies, the `WritableStream` constructor, `pipeTo`, and `pipeThrough` are unsupported.
The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported.
The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side.
A `decodeStrings` or `defaultEncoding` value known only at run time is unsupported after an option member whose value has effects (a call or an assignment).
In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write).
`setDefaultEncoding()` takes any encoding Node knows.
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
`on`, `once`, and `addListener` return the interface, so registrations chain.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
@@ -42,7 +45,7 @@
The output's `'resize'` event does not redraw the line.
The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end.
In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself.
`rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`.
`rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node; `rl.cursor` is `undefined` on an interface that is not a terminal.
`rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node.
In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported.
In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`.
@@ -1,4 +1,4 @@
- **Synchronous iteration:** Checked `for...of` follows live iterator methods, including compiled class methods and abrupt-completion cleanup.
Mapper-less checked `Array.from` accepts those iterators and plain array-like objects.
Sparse checked arrays and general borrowed iterator factories are unsupported.
Direct statically typed class loops also restrict iterator `return` and `throw` methods.
Direct statically typed class loops accept iterators that are class instances or object literals; a loop that exits early calls the iterator's zero-parameter `return()`, as Node's IteratorClose does.
@@ -1,6 +1,7 @@
- **Text encoding:** `TextEncoder` and `TextDecoder` can be stored in fields, arguments, return values, arrays, and closures.
Encoding supports UTF-8, including `encodeInto` into a Uint8Array or Buffer view.
Codec properties expose the canonical encoding and decoder flags.
Codecs held in `any` or `unknown` storage keep their identity and streaming state, and dynamic code can call `encode`, `encodeInto` and `decode`, with options objects, and read their properties.
Decoding accepts recognized WHATWG labels, including runtime labels and omitted or undefined encodings.
Literal constructor options support `fatal` and `ignoreBOM`; literal decode options support streaming UTF-8, UTF-16, and single-byte encodings.
Other legacy streaming encodings, runtime options objects, inspection, JSON conversion, and module-scope declarations using `var` or lacking an initializer are unsupported.
@@ -1,9 +1,7 @@
- **URL:** Non-ASCII or percent-escaped hosts are unsupported.
Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
- **URL:** Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile.
`URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors.
`href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code.
Invalid component writes are ignored except for invalid `href` values, which throw; credentials are percent-encoded and ports accept valid leading digits.
Native URL assignments require statically known property keys; `+=` accepts primitive operands, while other assignment operators and object operands for `+=` are refused.
`origin` is read-only.
`searchParams` is a live view whose identity survives query and URL replacement.
Opaque paths retain their input bytes without WHATWG C0 encoding.
+1 -1
View File
@@ -1,7 +1,7 @@
[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.
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, follows callback results only for callbacks written at a `map` or `filter` call that scriptc runs inline, 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,6 +1,7 @@
- **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.
A class instance whose `[Symbol.asyncIterator]()` returns an object literal, or that has only `[Symbol.iterator]()`, iterates through the checked-dynamic async protocol, and an early exit awaits the iterator's `return()`.
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.
@@ -2,5 +2,12 @@ Node-API (N-API) and V8 `.node` addons depend on Node.js itself, so compiled pro
Direct `createRequire` calls to local addons are rejected at compile time.
A package that requires an addon needs a JavaScript implementation that compiles, or a replacement through native FFI.
A package that looks for an optional addon at run time compiles with `--npm-static` when the lookup is inside a `try` block.
There, a `createRequire` require call with a computed specifier, and a `require.resolve` or `require.resolve.paths` call with a computed request, evaluate their arguments and then throw an error.
Its `require.resolve` read as a value is a function that throws when called.
Coverage lists these as deferred to runtime.
The package's `catch` clause then runs as it does in Node.js when the addon is missing.
Outside a `try` block, the same uses keep the package refused.
Use [Native FFI](/docs/ffi#replacing-a-node-api-addon) to call a plain C ABI function linked from an object or static archive.
The guide includes a replacement for a small Node-API helper.
@@ -4,6 +4,8 @@ Empty record layouts are also snapshots because they have no property storage.
`scriptc coverage` and `scriptc build` report `SC6001` where a copied value is then written or compared, whether the record was passed as an argument, stored in a typed binding, asserted with `as`, assigned to a typed binding later, or stored as an element of a typed array literal.
Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage.
A record or class instance converted to a string-keyed dictionary type such as `Record<string, string>`, directly or through a `{}`, `object` or `unknown` binding or parameter, keeps its identity when the program writes through, compares or inspects values of that dictionary type.
Every dictionary type with the same value type then uses checked dynamic storage, which is slower than a native dictionary.
Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js.
These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects.
A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy.
@@ -0,0 +1,14 @@
`tlsSocket.getPeerCertificate()`, `getCertificate()`, `getPeerX509Certificate()`, and `getX509Certificate()` follow Node, including the `detailed` issuer chain, on sockets whose type is known.
On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported.
Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`.
A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does.
A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs.
The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node.
In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused.
`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused.
`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case.
A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server.
The `'tlsClientError'` event reports Node's error for a failed handshake: OpenSSL's code and reason, or `socket hang up` for a client that leaves.
OpenSSL's messages leave out the thread identifier Node prints before `error:` and the source location after the reason.
A failure whose OpenSSL reason the runtime does not know, such as a malformed client hello, is reported as an error with code `SC2020` that says so.
The X509Certificate those accessors return has the same refusals as any other certificate (see Crypto), so in JavaScript narrow it, for example with `if (cert)`, before reading its fields.
@@ -0,0 +1,3 @@
TLS servers send no session tickets, so a connection to them cannot be resumed.
A client's `'session'` event fires once per connection, when its handshake finishes and before `secureConnect`.
Node fires it for each ticket the server sends, after `secureConnect`, and OpenSSL servers send two.
@@ -1,4 +1,5 @@
- **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding.
A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported.
Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported.
JavaScript object spreads copy represented own symbol fields.
Equal registry keys share a field across modules.
@@ -15,3 +15,6 @@ JavaScript argument and return conversions preserve native array references; oth
Supported operations on `unknown` include truthiness, `typeof` narrowing, property access, `+`, `switch`, and `throw`.
Cast before using other operations.
In JavaScript, a call whose untyped argument could select an overload with another result type returns a checked value, whichever overload the type checker picks, and so do the unannotated bindings and functions that hold or return it.
In TypeScript, an `any` argument keeps the overload the type checker picks, and a value that selects another overload at runtime throws a `TypeError` where the result is used.
+2 -1
View File
@@ -10,9 +10,10 @@
"build": "fumadocs-mdx && next build",
"start": "next start",
"typecheck": "fumadocs-mdx && next typegen && tsc --noEmit",
"test:unit": "node --test test/compatibility-matrix.test.mjs",
"test:routes": "node scripts/test-routes.mjs",
"compat:check": "pnpm --dir .. node-compat:check",
"check": "pnpm compat:check && pnpm typecheck && pnpm build && pnpm test:routes"
"check": "pnpm compat:check && pnpm typecheck && pnpm test:unit && pnpm build && pnpm test:routes"
},
"dependencies": {
"@vercel/geistdocs": "2.7.8",
+157 -198
View File
@@ -4,52 +4,28 @@
// empty after an edit.
// @refresh reset
import { useEffect, useMemo, useState } from "react";
import { memo, startTransition, useCallback, useDeferredValue, useEffect, useId, useMemo, useState, type FocusEvent, type KeyboardEvent, type ReactNode } from "react";
import {
buildTree,
expandableIds,
filterTree,
flattenTree,
nodeCount,
rowMatcher,
searchTexts,
statusCounts,
statuses,
type NodeStability,
type Row,
type Snapshot,
type Status,
type Verification,
} from "@/lib/compatibility-tree";
// Set by next.config.mjs from the committed artifact.
const artifactVersion = process.env.COMPATIBILITY_ARTIFACT_VERSION;
const rowCount = Number(process.env.COMPATIBILITY_ROW_COUNT);
type Status =
| "supported"
| "partial"
| "refused"
| "not-implemented"
| "by-design"
| "unreviewed"
| "not-applicable";
type VerificationBasis =
| "test-backed"
| "explicit-refusal"
| "verified-absence"
| "declared-gap"
| "registry-gap"
| "architectural-policy"
| "unreviewed"
| "not-applicable"
| "derived";
type Verification = { label: string; detail: string };
type Tier = { status: Status; detail: string; verification: VerificationBasis };
type NodeStability = { index: string; level: number; text: string; inherited: boolean } | null;
type Row = {
id: string;
chapter: string;
kind: string;
name: string;
signature: string;
label: string;
apiSymbol: string;
depth: number;
scope: "api" | "documentation" | "metadata" | "configuration";
anchor: string;
nodeStability: NodeStability;
static: Tier;
};
type Chapter = { slug: string; title: string; entries: number; apiEntries: number };
type Snapshot = { nodeVersion: string; verificationBases: Record<VerificationBasis, Verification>; chapters: Chapter[]; rows: Row[] };
type TreeNode = { row: Row; children: TreeNode[] };
type VisibleNode = { node: TreeNode; level: number };
const labels: Record<Status, string> = {
supported: "Supported",
partial: "Partial",
@@ -70,7 +46,6 @@ const styles: Record<Status, string> = {
"not-applicable": "border-gray-alpha-400 bg-background-200 text-gray-700",
};
const statuses = ["supported", "partial", "not-implemented", "refused", "by-design", "unreviewed", "not-applicable"] as const;
const stabilityStyles: Record<number, string> = {
0: "border-red-500/40 bg-red-100 text-red-900 dark:bg-red-200",
1: "border-amber-500/50 bg-amber-100 text-amber-900 dark:bg-amber-200",
@@ -78,114 +53,83 @@ const stabilityStyles: Record<number, string> = {
3: "border-gray-alpha-500 bg-gray-alpha-100 text-gray-900",
};
function StatusBadge({ status, detail, verification }: { status: Status; detail?: string; verification?: Verification }) {
const tooltip = verification ? `${detail ?? ""}${detail ? "\n\n" : ""}Verification: ${verification.label}. ${verification.detail}` : detail;
/** A badge's tooltip, mounted only while the badge is hovered or has
* keyboard focus (the :focus-visible rule the CSS-only tooltip used), and
* named by aria-describedby while open. Hover and focus are tracked apart,
* so moving the pointer away keeps a keyboard-focused badge's tooltip.
* Escape dismisses it until the next hover or focus. */
function BadgeTooltip({ tooltipClass, children, badge }: { tooltipClass: string; children: ReactNode; badge: ReactNode }) {
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
const open = hovered || focused;
const id = useId();
const enter = useCallback(() => setHovered(true), []);
const leave = useCallback(() => setHovered(false), []);
const focus = useCallback((event: FocusEvent<HTMLSpanElement>) => {
if (event.currentTarget.matches(":focus-visible")) setFocused(true);
}, []);
const blur = useCallback(() => setFocused(false), []);
const key = useCallback((event: KeyboardEvent<HTMLSpanElement>) => {
if (event.key !== "Escape") return;
setHovered(false);
setFocused(false);
}, []);
return (
<span className="group relative inline-flex" tabIndex={tooltip ? 0 : undefined}>
<span
className={`inline-flex whitespace-nowrap rounded-full border px-2 py-0.5 text-[11px] font-medium ${styles[status]}`}
title={tooltip}
>
{labels[status]}
</span>
{tooltip ? (
<span role="tooltip" className="pointer-events-none invisible absolute left-0 top-full z-30 mt-2 w-64 rounded-md border border-gray-alpha-500 bg-background-100 p-2 text-left text-xs font-normal leading-5 text-gray-1000 opacity-0 shadow-popover transition-opacity group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100">
{detail ? <span className="block">{detail}</span> : null}
{verification ? <span className={detail ? "mt-2 block border-t border-gray-alpha-400 pt-2" : "block"}><strong>Verification:</strong> {verification.label}. {verification.detail}</span> : null}
<span
className="relative inline-flex"
tabIndex={0}
aria-describedby={open ? id : undefined}
onMouseEnter={enter}
onMouseLeave={leave}
onFocus={focus}
onBlur={blur}
onKeyDown={key}
>
{badge}
{open ? (
<span id={id} role="tooltip" className={`pointer-events-none absolute top-full z-30 mt-2 rounded-md border border-gray-alpha-500 bg-background-100 p-2 text-left text-xs font-normal leading-5 text-gray-1000 opacity-100 shadow-popover transition-opacity starting:opacity-0 ${tooltipClass}`}>
{children}
</span>
) : null}
</span>
);
}
function StabilityBadge({ stability }: { stability: NodeStability }) {
export const StatusBadge = memo(function StatusBadge({ status, detail, verification }: { status: Status; detail?: string; verification?: Verification }) {
const tooltip = verification ? `${detail ?? ""}${detail ? "\n\n" : ""}Verification: ${verification.label}. ${verification.detail}` : detail;
const badge = (
<span
className={`inline-flex whitespace-nowrap rounded-full border px-2 py-0.5 text-[11px] font-medium ${styles[status]}`}
title={tooltip}
>
{labels[status]}
</span>
);
if (!tooltip) return <span className="relative inline-flex">{badge}</span>;
return (
<BadgeTooltip tooltipClass="left-0 w-64" badge={badge}>
{detail ? <span className="block">{detail}</span> : null}
{verification ? <span className={detail ? "mt-2 block border-t border-gray-alpha-400 pt-2" : "block"}><strong>Verification:</strong> {verification.label}. {verification.detail}</span> : null}
</BadgeTooltip>
);
});
export const StabilityBadge = memo(function StabilityBadge({ stability }: { stability: NodeStability }) {
if (stability === null) return <span className="text-xs text-gray-700" title="Node.js does not publish a stability index for this entry.">—</span>;
const detail = `Node.js Stability ${stability.index}: ${stability.text}${stability.inherited ? " (inherited from the nearest parent section)" : ""}`;
return (
<span className="group relative inline-flex" tabIndex={0}>
<span className={`inline-flex min-w-7 justify-center rounded-full border px-2 py-0.5 text-[11px] font-medium ${stabilityStyles[stability.level] ?? stabilityStyles[3]}`} title={detail}>
{stability.index}
</span>
<span role="tooltip" className="pointer-events-none invisible absolute left-1/2 top-full z-30 mt-2 w-72 -translate-x-1/2 rounded-md border border-gray-alpha-500 bg-background-100 p-2 text-left text-xs font-normal leading-5 text-gray-1000 opacity-0 shadow-popover transition-opacity group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100">
{detail}
</span>
</span>
);
}
function statusCounts(rows: Row[], tier: "static") {
return rows.reduce<Record<Status, number>>(
(counts, row) => {
if (row.scope !== "api" || row.depth === 0) return counts;
counts[row[tier].status] += 1;
return counts;
},
{
supported: 0,
partial: 0,
refused: 0,
"not-implemented": 0,
"by-design": 0,
unreviewed: 0,
"not-applicable": 0,
},
);
}
/** Node's all.json is preorder with an explicit heading depth. */
function buildTree(rows: Row[]): TreeNode[] {
const roots: TreeNode[] = [];
const stack: TreeNode[] = [];
for (const row of rows) {
const node: TreeNode = { row, children: [] };
while (stack.length > row.depth) stack.pop();
const parent = stack[row.depth - 1];
if (row.depth === 0 || parent === undefined) roots.push(node);
else parent.children.push(node);
stack[row.depth] = node;
stack.length = row.depth + 1;
}
return roots;
}
function filterTree(nodes: TreeNode[], matches: (row: Row) => boolean): TreeNode[] {
const filtered: TreeNode[] = [];
for (const node of nodes) {
const children = filterTree(node.children, matches);
if (matches(node.row) || children.length > 0) filtered.push({ row: node.row, children });
}
return filtered;
}
function flattenTree(nodes: TreeNode[], expanded: Set<string>, forceExpanded: boolean): VisibleNode[] {
const visible: VisibleNode[] = [];
const visit = (items: TreeNode[], level: number) => {
for (const node of items) {
visible.push({ node, level });
if (node.children.length > 0 && (forceExpanded || expanded.has(node.row.id))) {
visit(node.children, level + 1);
<BadgeTooltip
tooltipClass="left-1/2 w-72 -translate-x-1/2"
badge={
<span className={`inline-flex min-w-7 justify-center rounded-full border px-2 py-0.5 text-[11px] font-medium ${stabilityStyles[stability.level] ?? stabilityStyles[3]}`} title={detail}>
{stability.index}
</span>
}
}
};
visit(nodes, 1);
return visible;
}
function nodeCount(nodes: TreeNode[]): number {
return nodes.reduce((count, node) => count + 1 + nodeCount(node.children), 0);
}
function expandableIds(nodes: TreeNode[]): Set<string> {
const ids = new Set<string>();
const visit = (items: TreeNode[]) => {
for (const node of items) {
if (node.children.length > 0) ids.add(node.row.id);
visit(node.children);
}
};
visit(nodes);
return ids;
}
>
{detail}
</BadgeTooltip>
);
});
function Chevron({ open }: { open: boolean }) {
return (
@@ -195,6 +139,47 @@ function Chevron({ open }: { open: boolean }) {
);
}
type MatrixRowProps = {
row: Row;
level: number;
hasChildren: boolean;
open: boolean;
filtering: boolean;
displayName: string;
href: string;
verification: Verification | undefined;
onToggle: (id: string) => void;
};
/** One table row. Memoized on its own values, so a filter or expansion
* change re-renders only the rows whose values changed. */
const MatrixRow = memo(function MatrixRow({ row, level, hasChildren, open, filtering, displayName, href, verification, onToggle }: MatrixRowProps) {
const isRoot = level === 1;
return (
<tr aria-level={level} aria-expanded={hasChildren ? open : undefined} className={`align-top hover:bg-gray-alpha-100/60 ${isRoot ? "bg-background-200/60" : ""}`}>
<td>
<div className="flex items-start gap-1.5" style={{ paddingLeft: `${(level - 1) * 18}px` }}>
{hasChildren ? (
<button type="button" onClick={() => onToggle(row.id)} disabled={filtering} aria-label={`${open ? "Collapse" : "Expand"} ${displayName}`} className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded text-gray-800 hover:bg-gray-alpha-200 hover:text-gray-1000 disabled:cursor-default disabled:hover:bg-transparent">
<Chevron open={open} />
</button>
) : <span className="h-5 w-5 shrink-0" />}
<div className="min-w-0">
<a href={href} target="_blank" rel="noopener noreferrer" className={`${isRoot ? "font-sans text-sm font-medium" : "font-mono text-xs"} text-gray-1000 underline decoration-gray-alpha-500 underline-offset-2 hover:decoration-gray-1000`}>
{displayName}
</a>
<div className="mt-1 text-[11px] text-gray-800">
{row.kind}{row.apiSymbol && row.apiSymbol !== row.signature && !isRoot ? ` · ${row.apiSymbol}` : ""}
</div>
</div>
</div>
</td>
<td className="text-center"><StabilityBadge stability={row.nodeStability} /></td>
<td><StatusBadge status={row.static.status} detail={row.static.detail} verification={verification} /></td>
</tr>
);
});
export function NodeCompatibilityMatrix() {
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
const [loadError, setLoadError] = useState(false);
@@ -202,15 +187,16 @@ export function NodeCompatibilityMatrix() {
const [staticFilter, setStaticFilter] = useState<"all" | Status>("all");
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
const rows = snapshot?.rows ?? [];
const chapters = snapshot?.chapters ?? [];
const rows = useMemo(() => snapshot?.rows ?? [], [snapshot]);
const chapters = useMemo(() => snapshot?.chapters ?? [], [snapshot]);
const verificationBases = snapshot?.verificationBases;
const apiRowCount = useMemo(() => rows.filter((row) => row.scope === "api").length, [rows]);
const chapterTitles = useMemo(() => new Map(chapters.map((item) => [item.slug, item.title])), [chapters]);
const base = `https://nodejs.org/docs/v${snapshot?.nodeVersion ?? "24.22.0"}/api/`;
const staticCounts = useMemo(() => statusCounts(rows, "static"), [rows]);
const staticCounts = useMemo(() => statusCounts(rows), [rows]);
const visibleStatuses = useMemo(() => statuses.filter((status) => staticCounts[status] > 0), [staticCounts]);
const tree = useMemo(() => buildTree(rows), [rows]);
const texts = useMemo(() => searchTexts(rows, chapterTitles), [rows, chapterTitles]);
useEffect(() => {
const controller = new AbortController();
@@ -231,36 +217,23 @@ export function NodeCompatibilityMatrix() {
return () => controller.abort();
}, []);
const needle = query.trim().toLocaleLowerCase();
const hasResultFilter = needle !== "" || staticFilter !== "all";
const rowMatches = (row: Row) => {
if (staticFilter !== "all" && row.static.status !== staticFilter) return false;
if (!needle) return true;
return [chapterTitles.get(row.chapter), row.kind, row.name, row.signature, row.apiSymbol, row.static.detail]
.join(" ")
.toLocaleLowerCase()
.includes(needle);
};
// The search field and the status select update at once; the table
// follows the deferred values, so input stays responsive while the rows
// re-render.
const deferredQuery = useDeferredValue(query);
const deferredStatic = useDeferredValue(staticFilter);
const needle = deferredQuery.trim().toLocaleLowerCase();
const hasResultFilter = needle !== "" || deferredStatic !== "all";
const resultTree = useMemo(
() => hasResultFilter ? filterTree(tree, rowMatches) : tree,
// rowMatches closes over these filter values.
// eslint-disable-next-line react-hooks/exhaustive-deps
[hasResultFilter, needle, staticFilter, tree],
const result = useMemo(
() => hasResultFilter ? filterTree(tree, rowMatcher(texts, needle, deferredStatic)) : { nodes: tree, matched: nodeCount(tree) },
[hasResultFilter, needle, deferredStatic, texts, tree],
);
const visible = useMemo(
() => flattenTree(resultTree, expanded, hasResultFilter),
[expanded, hasResultFilter, resultTree],
);
const matchedCount = useMemo(
() => hasResultFilter ? tree.reduce((count, node) => {
const visit = (item: TreeNode): number => (rowMatches(item.row) ? 1 : 0) + item.children.reduce((sum, child) => sum + visit(child), 0);
return count + visit(node);
}, 0) : nodeCount(tree),
// rowMatches closes over these filter values.
// eslint-disable-next-line react-hooks/exhaustive-deps
[hasResultFilter, needle, staticFilter, tree],
() => flattenTree(result.nodes, expanded, hasResultFilter),
[expanded, hasResultFilter, result],
);
const matchedCount = result.matched;
const changeQuery = (value: string) => setQuery(value);
const resetFilters = () => {
@@ -268,12 +241,12 @@ export function NodeCompatibilityMatrix() {
setStaticFilter("all");
setExpanded(new Set());
};
const toggle = (id: string) => setExpanded((current) => {
const toggle = useCallback((id: string) => setExpanded((current) => {
const next = new Set(current);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}), []);
useEffect(() => {
if (!snapshot) return;
@@ -316,8 +289,8 @@ export function NodeCompatibilityMatrix() {
<span aria-live="polite">{visible.length.toLocaleString()} visible · {matchedCount.toLocaleString()} {hasResultFilter ? "matching" : "API"} rows</span>
{!hasResultFilter ? (
<span className="flex gap-1">
<button type="button" onClick={() => setExpanded(expandableIds(tree))} className="rounded px-1.5 py-1 text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000">Expand all</button>
<button type="button" onClick={() => setExpanded(new Set())} className="rounded px-1.5 py-1 text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000">Collapse all</button>
<button type="button" onClick={() => startTransition(() => setExpanded(expandableIds(tree)))} className="rounded px-1.5 py-1 text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000">Expand all</button>
<button type="button" onClick={() => startTransition(() => setExpanded(new Set()))} className="rounded px-1.5 py-1 text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000">Collapse all</button>
</span>
) : null}
</div>
@@ -335,33 +308,19 @@ export function NodeCompatibilityMatrix() {
<tbody>
{visible.map(({ node, level }) => {
const row = node.row;
const hasChildren = node.children.length > 0;
const open = hasResultFilter || expanded.has(row.id);
const href = `${base}${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`;
const isRoot = level === 1;
const displayName = isRoot ? chapterTitles.get(row.chapter) ?? row.label : row.label;
return (
<tr key={row.id} aria-level={level} aria-expanded={hasChildren ? open : undefined} className={`align-top hover:bg-gray-alpha-100/60 ${isRoot ? "bg-background-200/60" : ""}`}>
<td>
<div className="flex items-start gap-1.5" style={{ paddingLeft: `${(level - 1) * 18}px` }}>
{hasChildren ? (
<button type="button" onClick={() => toggle(row.id)} disabled={hasResultFilter} aria-label={`${open ? "Collapse" : "Expand"} ${displayName}`} className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded text-gray-800 hover:bg-gray-alpha-200 hover:text-gray-1000 disabled:cursor-default disabled:hover:bg-transparent">
<Chevron open={open} />
</button>
) : <span className="h-5 w-5 shrink-0" />}
<div className="min-w-0">
<a href={href} target="_blank" rel="noopener noreferrer" className={`${isRoot ? "font-sans text-sm font-medium" : "font-mono text-xs"} text-gray-1000 underline decoration-gray-alpha-500 underline-offset-2 hover:decoration-gray-1000`}>
{displayName}
</a>
<div className="mt-1 text-[11px] text-gray-800">
{row.kind}{row.apiSymbol && row.apiSymbol !== row.signature && !isRoot ? ` · ${row.apiSymbol}` : ""}
</div>
</div>
</div>
</td>
<td className="text-center"><StabilityBadge stability={row.nodeStability} /></td>
<td><StatusBadge status={row.static.status} detail={row.static.detail} verification={verificationBases?.[row.static.verification]} /></td>
</tr>
<MatrixRow
key={row.id}
row={row}
level={level}
hasChildren={node.children.length > 0}
open={hasResultFilter || expanded.has(row.id)}
filtering={hasResultFilter}
displayName={level === 1 ? chapterTitles.get(row.chapter) ?? row.label : row.label}
href={`${base}${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`}
verification={verificationBases?.[row.static.verification]}
onToggle={toggle}
/>
);
})}
</tbody>
+134 -134
View File
@@ -11476,9 +11476,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -11660,9 +11660,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -13822,9 +13822,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13845,9 +13845,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13868,9 +13868,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13891,9 +13891,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13914,9 +13914,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13937,9 +13937,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13960,9 +13960,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13983,9 +13983,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -14006,9 +14006,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -14029,9 +14029,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -14236,9 +14236,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -18268,9 +18268,9 @@
"anchor": "err_brotli_invalid_param",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -23790,9 +23790,9 @@
"anchor": "err_tls_required_server_name",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -35971,9 +35971,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -67484,9 +67484,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -67599,9 +67599,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -67783,9 +67783,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -67921,9 +67921,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -68013,9 +68013,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -68059,9 +68059,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -68174,9 +68174,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -68312,9 +68312,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -68335,9 +68335,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -71509,9 +71509,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -81657,9 +81657,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented static async-iteration subset.",
"verification": "test-backed"
}
},
{
@@ -84302,9 +84302,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
"verification": "test-backed"
}
},
{
@@ -84325,9 +84325,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
"verification": "test-backed"
}
},
{
@@ -84349,7 +84349,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84372,7 +84372,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84395,7 +84395,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84418,7 +84418,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84441,7 +84441,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84464,7 +84464,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84487,7 +84487,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84510,7 +84510,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84533,7 +84533,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84556,7 +84556,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84579,7 +84579,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84602,7 +84602,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84625,7 +84625,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -84716,9 +84716,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -84739,9 +84739,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -84969,9 +84969,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -84992,9 +84992,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85015,9 +85015,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85038,9 +85038,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85499,7 +85499,7 @@
},
"static": {
"status": "partial",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.",
"detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.",
"verification": "test-backed"
}
},
@@ -85521,9 +85521,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().",
"verification": "test-backed"
}
},
{
@@ -85567,9 +85567,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85590,9 +85590,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85613,9 +85613,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -85636,9 +85636,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
+157
View File
@@ -0,0 +1,157 @@
// The compatibility matrix's data model: the ledger rows as a tree, and the
// search and filter passes over it. Kept free of React so the passes can be
// tested directly.
export type Status =
| "supported"
| "partial"
| "refused"
| "not-implemented"
| "by-design"
| "unreviewed"
| "not-applicable";
export type VerificationBasis =
| "test-backed"
| "explicit-refusal"
| "verified-absence"
| "declared-gap"
| "registry-gap"
| "architectural-policy"
| "unreviewed"
| "not-applicable"
| "derived";
export type Verification = { label: string; detail: string };
export type Tier = { status: Status; detail: string; verification: VerificationBasis };
export type NodeStability = { index: string; level: number; text: string; inherited: boolean } | null;
export type Row = {
id: string;
chapter: string;
kind: string;
name: string;
signature: string;
label: string;
apiSymbol: string;
depth: number;
scope: "api" | "documentation" | "metadata" | "configuration";
anchor: string;
nodeStability: NodeStability;
static: Tier;
};
export type Chapter = { slug: string; title: string; entries: number; apiEntries: number };
export type Snapshot = {
nodeVersion: string;
verificationBases: Record<VerificationBasis, Verification>;
chapters: Chapter[];
rows: Row[];
};
export type TreeNode = { row: Row; children: TreeNode[] };
export type VisibleNode = { node: TreeNode; level: number };
export const statuses = ["supported", "partial", "not-implemented", "refused", "by-design", "unreviewed", "not-applicable"] as const;
export function statusCounts(rows: Row[]): Record<Status, number> {
return rows.reduce<Record<Status, number>>(
(counts, row) => {
if (row.scope !== "api" || row.depth === 0) return counts;
counts[row.static.status] += 1;
return counts;
},
{
supported: 0,
partial: 0,
refused: 0,
"not-implemented": 0,
"by-design": 0,
unreviewed: 0,
"not-applicable": 0,
},
);
}
/** Node's all.json is preorder with an explicit heading depth. */
export function buildTree(rows: Row[]): TreeNode[] {
const roots: TreeNode[] = [];
const stack: TreeNode[] = [];
for (const row of rows) {
const node: TreeNode = { row, children: [] };
while (stack.length > row.depth) stack.pop();
const parent = stack[row.depth - 1];
if (row.depth === 0 || parent === undefined) roots.push(node);
else parent.children.push(node);
stack[row.depth] = node;
stack.length = row.depth + 1;
}
return roots;
}
/** The lowercase text a search matches against, built once per row: the
* chapter title, kind, name, signature, API symbol and support detail. */
export function searchTexts(rows: Row[], chapterTitles: ReadonlyMap<string, string>): Map<string, string> {
return new Map(
rows.map((row) => [
row.id,
[chapterTitles.get(row.chapter), row.kind, row.name, row.signature, row.apiSymbol, row.static.detail]
.join(" ")
.toLocaleLowerCase(),
]),
);
}
/** The rows a status filter and a lowercase search needle keep. */
export function rowMatcher(
texts: ReadonlyMap<string, string>,
needle: string,
status: "all" | Status,
): (row: Row) => boolean {
return (row) => {
if (status !== "all" && row.static.status !== status) return false;
return needle === "" || (texts.get(row.id) ?? "").includes(needle);
};
}
/** The matching rows with their ancestors, and the number of matching
* rows, in one pass. */
export function filterTree(nodes: TreeNode[], matches: (row: Row) => boolean): { nodes: TreeNode[]; matched: number } {
let matched = 0;
const visit = (items: TreeNode[]): TreeNode[] => {
const filtered: TreeNode[] = [];
for (const node of items) {
const children = visit(node.children);
const own = matches(node.row);
if (own) matched += 1;
if (own || children.length > 0) filtered.push({ row: node.row, children });
}
return filtered;
};
return { nodes: visit(nodes), matched };
}
export function flattenTree(nodes: TreeNode[], expanded: ReadonlySet<string>, forceExpanded: boolean): VisibleNode[] {
const visible: VisibleNode[] = [];
const visit = (items: TreeNode[], level: number) => {
for (const node of items) {
visible.push({ node, level });
if (node.children.length > 0 && (forceExpanded || expanded.has(node.row.id))) {
visit(node.children, level + 1);
}
}
};
visit(nodes, 1);
return visible;
}
export function nodeCount(nodes: TreeNode[]): number {
return nodes.reduce((count, node) => count + 1 + nodeCount(node.children), 0);
}
export function expandableIds(nodes: TreeNode[]): Set<string> {
const ids = new Set<string>();
const visit = (items: TreeNode[]) => {
for (const node of items) {
if (node.children.length > 0) ids.add(node.row.id);
visit(node.children);
}
};
visit(nodes);
return ids;
}
+115
View File
@@ -0,0 +1,115 @@
import assert from "node:assert/strict";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { test } from "node:test";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import ts from "typescript";
import {
buildTree,
expandableIds,
filterTree,
flattenTree,
nodeCount,
rowMatcher,
searchTexts,
statusCounts,
} from "../src/lib/compatibility-tree.ts";
const row = (id, depth, label, status, detail = "") => ({
id,
chapter: "zlib",
kind: depth === 0 ? "module" : "method",
name: label,
signature: label,
label,
apiSymbol: label,
depth,
scope: "api",
anchor: "",
nodeStability: null,
static: { status, detail, verification: "test-backed" },
});
const rows = [
row("root", 0, "zlib", "partial"),
row("a", 1, "zlib.gzipSync()", "supported", "Lowered with Node's options"),
row("b", 1, "zlib.Gzip", "partial"),
row("b1", 2, "gzip.flush()", "supported"),
row("c", 1, "zlib.zstdCompress()", "not-implemented"),
];
const titles = new Map([["zlib", "Zlib"]]);
test("the tree follows the rows' heading depths", () => {
const tree = buildTree(rows);
assert.equal(tree.length, 1);
assert.deepEqual(tree[0].children.map((node) => node.row.id), ["a", "b", "c"]);
assert.deepEqual(tree[0].children[1].children.map((node) => node.row.id), ["b1"]);
assert.equal(nodeCount(tree), 5);
assert.deepEqual([...expandableIds(tree)].sort(), ["b", "root"]);
});
test("filtering keeps matching rows with their ancestors and counts the matches once", () => {
const tree = buildTree(rows);
const texts = searchTexts(rows, titles);
const byText = filterTree(tree, rowMatcher(texts, "flush", "all"));
assert.equal(byText.matched, 1);
assert.deepEqual(flattenTree(byText.nodes, new Set(), true).map(({ node, level }) => `${node.row.id}@${level}`), ["root@1", "b@2", "b1@3"]);
const byDetail = filterTree(tree, rowMatcher(texts, "node's options", "all"));
assert.deepEqual(flattenTree(byDetail.nodes, new Set(), true).map(({ node }) => node.row.id), ["root", "a"]);
const byStatus = filterTree(tree, rowMatcher(texts, "", "supported"));
assert.equal(byStatus.matched, 2);
const both = filterTree(tree, rowMatcher(texts, "gzip", "partial"));
assert.equal(both.matched, 1);
assert.equal(filterTree(tree, rowMatcher(texts, "brotli", "all")).nodes.length, 0);
});
test("collapsed rows hide their children until expanded", () => {
const tree = buildTree(rows);
assert.deepEqual(flattenTree(tree, new Set(), false).map(({ node }) => node.row.id), ["root"]);
assert.deepEqual(flattenTree(tree, new Set(["root"]), false).map(({ node }) => node.row.id), ["root", "a", "b", "c"]);
assert.deepEqual(flattenTree(tree, new Set(["root", "b"]), false).map(({ node }) => node.row.id), ["root", "a", "b", "b1", "c"]);
});
test("status counts cover API rows below the chapters", () => {
const counts = statusCounts(rows);
assert.equal(counts.supported, 2);
assert.equal(counts.partial, 1);
assert.equal(counts["not-implemented"], 1);
});
/** The matrix component, compiled for Node from its TSX source. */
async function loadMatrix() {
const docs = new URL("../", import.meta.url);
const source = await readFile(new URL("src/components/node-compatibility-matrix.tsx", docs), "utf8");
const { outputText } = ts.transpileModule(source, {
compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
});
const lib = pathToFileURL(fileURLToPath(new URL("src/lib/compatibility-tree.ts", docs))).href;
const dir = new URL("node_modules/.cache/compatibility-matrix-test/", docs);
await mkdir(dir, { recursive: true });
const file = new URL("node-compatibility-matrix.mjs", dir);
await writeFile(file, outputText.replace('"@/lib/compatibility-tree"', JSON.stringify(lib)));
return import(file.href);
}
test("badges render without their tooltips until hovered or focused", async () => {
const { StatusBadge, StabilityBadge } = await loadMatrix();
const status = renderToStaticMarkup(
createElement(StatusBadge, {
status: "partial",
detail: "Lowered with Node's options",
verification: { label: "Test-backed", detail: "Corpus programs cover it." },
}),
);
assert.ok(status.includes('tabindex="0"'), status);
assert.ok(status.includes('title="Lowered with Node&#x27;s options'), status);
assert.ok(!status.includes('role="tooltip"'), status);
assert.ok(!status.includes("aria-describedby"), status);
const stability = renderToStaticMarkup(
createElement(StabilityBadge, { stability: { index: "2", level: 2, text: "Stable", inherited: false } }),
);
assert.ok(stability.includes('tabindex="0"'), stability);
assert.ok(!stability.includes('role="tooltip"'), stability);
const legend = renderToStaticMarkup(createElement(StatusBadge, { status: "supported" }));
assert.ok(!legend.includes("tabindex"), legend);
});
@@ -1387,7 +1387,20 @@
"evidence": "derived:descendants",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "chapter"
@@ -1437,7 +1450,20 @@
"evidence": "compiler-dedicated:AsyncLocalStorage",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "exact"
@@ -1510,7 +1536,20 @@
"evidence": "compiler-dedicated:asyncLocalStorage.disable",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "exact"
@@ -1537,7 +1576,20 @@
"evidence": "compiler-dedicated:asyncLocalStorage.getStore",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "exact"
@@ -1564,7 +1616,20 @@
"evidence": "compiler-dedicated:asyncLocalStorage.enterWith",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "exact"
@@ -1591,7 +1656,20 @@
"evidence": "compiler-dedicated:asyncLocalStorage.run",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "exact"
@@ -1618,7 +1696,20 @@
"evidence": "compiler-dedicated:asyncLocalStorage.exit",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "exact"
@@ -1922,6 +2013,19 @@
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs",
"tests/harness/node-test.test.ts"
]
},
@@ -2484,7 +2588,20 @@
"evidence": "compiler-dedicated:AsyncLocalStorage",
"tests": [
"tests/corpus/2213-async-local-storage.cjs",
"tests/corpus/5815-als-callback-context.ts"
"tests/corpus/5815-als-callback-context.ts",
"tests/corpus/7110-als-completion-callbacks.ts",
"tests/corpus/7111-als-bind-snapshot-options.ts",
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
"tests/corpus/7119-als-net-event-context.mjs",
"tests/corpus/7380-als-http-event-context.mjs",
"tests/corpus/7383-als-stream-events.ts",
"tests/corpus/7384-als-stream-events.mjs",
"tests/corpus/7385-als-child-process-events.ts",
"tests/corpus/7386-als-child-fork-events/main.mjs",
"tests/corpus/7387-als-http2-events.mjs",
"tests/corpus/7388-als-http2-compat-events.mjs",
"tests/corpus/7389-als-http2-secure-events.mjs",
"tests/corpus/7391-als-response-end-and-finished.mjs"
]
},
"anchorSource": "exact"
@@ -19277,6 +19394,8 @@
"tests": [
"tests/corpus/1358-crypto-random.ts",
"tests/corpus/1534-crypto-hash-chain.ts",
"tests/corpus/1568-x509-fingerprint.ts",
"tests/corpus/1572-x509-validity-date-gettime.ts",
"tests/corpus/2556-crypto-introspection.ts",
"tests/corpus/2890-crypto-static-utility.ts",
"tests/corpus/5250-cipher-aes-gcm.ts",
@@ -19319,6 +19438,7 @@
"tests/corpus/6709-dh-generate.ts",
"tests/corpus/6710-dh-encodings.cjs",
"tests/corpus/6711-dh-handles.ts",
"tests/corpus/6936-x509-certificate-fields.ts",
"tests/corpus/7055-cipher-streams.ts",
"tests/corpus/7056-cipher-stream-errors.ts",
"tests/corpus/7057-cipher-streams.mjs",
@@ -19326,12 +19446,20 @@
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/corpus/7122-webcrypto-node-crypto.ts",
"tests/corpus/7123-webcrypto-node-crypto.cjs",
"tests/corpus/7150-cipher-legacy-modes.ts",
"tests/corpus/7151-cipher-legacy-errors.cjs",
"tests/corpus/7152-crypto-prime.ts",
"tests/corpus/7153-crypto-prime-errors.cjs",
"tests/corpus/7154-crypto-cipher-info.cjs",
"tests/corpus/7155-cipher-aes-ccm.cjs",
"tests/corpus/7156-cipher-key-wrap.cjs",
"tests/corpus/7157-crypto-cipher-info-typed.ts",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts",
"tests/corpus/node24-crypto-kdf.ts",
@@ -19340,6 +19468,7 @@
"tests/fixtures/node-types/dh.ts",
"tests/fixtures/node-types/ecdh.ts",
"tests/fixtures/node-types/keys.ts",
"tests/fixtures/node-types/x509-methods.ts",
"tests/harness/npm-static.test.ts"
]
},
@@ -19719,6 +19848,8 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.createCipheriv",
"tests": [
"tests/corpus/7155-cipher-aes-ccm.cjs",
"tests/corpus/7156-cipher-key-wrap.cjs",
"tests/corpus/7150-cipher-legacy-modes.ts",
"tests/corpus/7151-cipher-legacy-errors.cjs",
"tests/corpus/5250-cipher-aes-gcm.ts",
@@ -19760,6 +19891,8 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.createDecipheriv",
"tests": [
"tests/corpus/7155-cipher-aes-ccm.cjs",
"tests/corpus/7156-cipher-key-wrap.cjs",
"tests/corpus/7150-cipher-legacy-modes.ts",
"tests/corpus/7151-cipher-legacy-errors.cjs",
"tests/corpus/5250-cipher-aes-gcm.ts",
@@ -20220,8 +20353,19 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.diffieHellman"
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.diffieHellman",
"tests": [
"tests/corpus/1358-crypto-random.ts",
"tests/corpus/1534-crypto-hash-chain.ts",
"tests/corpus/2556-crypto-introspection.ts",
"tests/corpus/2890-crypto-static-utility.ts",
"tests/harness/npm-static.test.ts",
"tests/corpus/node24-crypto-sha2.ts",
"tests/corpus/node24-crypto-kdf.ts",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts"
]
},
"anchorSource": "exact"
},
@@ -20434,8 +20578,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.getCipherInfo"
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.getCipherInfo",
"tests": [
"tests/corpus/7154-crypto-cipher-info.cjs",
"tests/corpus/7157-crypto-cipher-info-typed.ts"
]
},
"anchorSource": "exact"
},
@@ -23644,8 +23792,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:X509Certificate",
"tests": [
"tests/corpus/1568-x509-fingerprint.ts",
"tests/corpus/1572-x509-validity-date-gettime.ts",
"tests/corpus/6936-x509-certificate-fields.ts"
]
},
"anchorSource": "exact"
},
@@ -23667,8 +23820,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.checkEmail",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23690,8 +23850,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.checkHost",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23713,8 +23880,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.checkIP",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23736,8 +23910,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.checkIssued",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23759,8 +23940,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.checkPrivateKey",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23782,8 +23970,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.toJSON",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23805,8 +24000,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.toLegacyObject",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23828,8 +24030,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.toString",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -23851,8 +24060,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:x509.verify",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "exact"
},
@@ -24058,8 +24274,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-feature:crypto.publicKey",
"tests": [
"tests/corpus/7090-x509-check-names.ts",
"tests/corpus/7091-x509-certificate-keys.ts",
"tests/corpus/7092-x509-legacy-object.ts",
"tests/corpus/7093-x509-methods-js.mjs",
"tests/fixtures/node-types/x509-methods.ts"
]
},
"anchorSource": "ancestor"
},
@@ -32539,6 +32762,8 @@
"tests": [
"tests/corpus/6702-ecdh-errors.js",
"tests/corpus/6708-dh-errors.js",
"tests/corpus/7440-tls-server-add-context.ts",
"tests/corpus/7714-zlib-brotli-compress.cjs",
"tests/corpus/caught-system-error-fields.ts",
"tests/corpus/error-stack-frames.ts",
"tests/corpus/error-stack-limit.ts",
@@ -33331,8 +33556,11 @@
"anchor": "err_brotli_invalid_param",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-feature:errors.ERR_BROTLI_INVALID_PARAM",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -38868,8 +39096,11 @@
"anchor": "err_tls_required_server_name",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-dedicated:ERR_TLS_REQUIRED_SERVER_NAME",
"tests": [
"tests/corpus/7440-tls-server-add-context.ts"
]
},
"anchorSource": "exact"
},
@@ -52182,6 +52413,8 @@
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/7743-transform-stream-identity.ts",
"tests/corpus/7747-text-codec-untyped-slot.js",
"tests/corpus/text-codec-values/main.ts",
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-dylink/main.mjs",
@@ -53334,7 +53567,8 @@
"tests": [
"tests/corpus/1423-text-codec.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/corpus/3133-text-decoder-streaming.ts"
"tests/corpus/3133-text-decoder-streaming.ts",
"tests/corpus/7747-text-codec-untyped-slot.js"
]
},
"anchorSource": "exact"
@@ -53385,7 +53619,8 @@
"tests": [
"tests/corpus/1423-text-codec.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/corpus/3133-text-decoder-streaming.ts"
"tests/corpus/3133-text-decoder-streaming.ts",
"tests/corpus/7747-text-codec-untyped-slot.js"
]
},
"anchorSource": "exact"
@@ -53431,8 +53666,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:globals"
"status": "partial",
"evidence": "compiler-feature:globals.TransformStream",
"tests": [
"tests/corpus/7743-transform-stream-identity.ts"
]
},
"anchorSource": "exact"
},
@@ -95726,6 +95964,17 @@
"tests/corpus/6722-tls-min-max-version.ts",
"tests/corpus/7087-tls-socket-wrap.ts",
"tests/corpus/7088-tls-socket-connect-tunnel.ts",
"tests/corpus/7095-tls-peer-certificates.ts",
"tests/corpus/7096-tls-peer-certificates.mjs",
"tests/corpus/7097-tls-client-certificates.ts",
"tests/corpus/7098-tls-client-certificates-paths.ts",
"tests/corpus/7099-tls-client-certificates.mjs",
"tests/corpus/7440-tls-server-add-context.ts",
"tests/corpus/7441-tls-client-error.ts",
"tests/corpus/7442-tls-server-contexts.mjs",
"tests/fixtures/node-types/tls-client-certificates.ts",
"tests/fixtures/node-types/tls-peer-certificates.ts",
"tests/fixtures/node-types/tls-server-contexts.ts",
"tests/fixtures/node-types/tls-socket-wrap.ts"
]
},
@@ -96094,8 +96343,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:server.addContext",
"tests": [
"tests/corpus/7440-tls-server-add-context.ts",
"tests/corpus/7441-tls-client-error.ts",
"tests/corpus/7442-tls-server-contexts.mjs",
"tests/fixtures/node-types/tls-server-contexts.ts"
]
},
"anchorSource": "exact"
},
@@ -96221,8 +96476,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:server.setSecureContext",
"tests": [
"tests/corpus/7440-tls-server-add-context.ts",
"tests/corpus/7441-tls-client-error.ts",
"tests/corpus/7442-tls-server-contexts.mjs",
"tests/fixtures/node-types/tls-server-contexts.ts"
]
},
"anchorSource": "exact"
},
@@ -96405,8 +96666,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:'tlsClientError'",
"tests": [
"tests/corpus/7440-tls-server-add-context.ts",
"tests/corpus/7441-tls-client-error.ts",
"tests/corpus/7442-tls-server-contexts.mjs",
"tests/fixtures/node-types/tls-server-contexts.ts"
]
},
"anchorSource": "exact"
},
@@ -96548,8 +96815,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:tlsSocket.getCertificate",
"tests": [
"tests/corpus/7095-tls-peer-certificates.ts",
"tests/corpus/7096-tls-peer-certificates.mjs",
"tests/fixtures/node-types/tls-peer-certificates.ts"
]
},
"anchorSource": "exact"
},
@@ -96640,8 +96912,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:tlsSocket.getPeerCertificate",
"tests": [
"tests/corpus/7095-tls-peer-certificates.ts",
"tests/corpus/7096-tls-peer-certificates.mjs",
"tests/fixtures/node-types/tls-peer-certificates.ts"
]
},
"anchorSource": "exact"
},
@@ -96709,8 +96986,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:tlsSocket.getPeerX509Certificate",
"tests": [
"tests/corpus/7095-tls-peer-certificates.ts",
"tests/corpus/7096-tls-peer-certificates.mjs",
"tests/fixtures/node-types/tls-peer-certificates.ts"
]
},
"anchorSource": "exact"
},
@@ -96827,8 +97109,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:tlsSocket.getX509Certificate",
"tests": [
"tests/corpus/7095-tls-peer-certificates.ts",
"tests/corpus/7096-tls-peer-certificates.mjs",
"tests/fixtures/node-types/tls-peer-certificates.ts"
]
},
"anchorSource": "exact"
},
@@ -96968,8 +97255,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-dedicated:tlsSocket.authorizationError",
"tests": [
"tests/corpus/7097-tls-client-certificates.ts",
"tests/corpus/7098-tls-client-certificates-paths.ts",
"tests/corpus/7099-tls-client-certificates.mjs",
"tests/fixtures/node-types/tls-client-certificates.ts"
]
},
"anchorSource": "exact"
},
@@ -96991,8 +97284,14 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-feature:tls.authorized",
"tests": [
"tests/corpus/7097-tls-client-certificates.ts",
"tests/corpus/7098-tls-client-certificates-paths.ts",
"tests/corpus/7099-tls-client-certificates.mjs",
"tests/fixtures/node-types/tls-client-certificates.ts"
]
},
"anchorSource": "ancestor"
},
@@ -99336,6 +99635,7 @@
"evidence": "derived:descendants",
"tests": [
"packages/runtime/test/url-legacy.test.ts",
"tests/corpus/1794-searchparams-url-live.ts",
"tests/corpus/5220-url-parse-warnings.ts",
"tests/corpus/5221-url-parse-shapes.ts",
"tests/corpus/5222-url-parse-hosts.ts",
@@ -100594,8 +100894,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:url"
"status": "partial",
"evidence": "surface-manifest:node-builtin.url.urlToHttpOptions",
"tests": [
"tests/corpus/1794-searchparams-url-live.ts"
]
},
"anchorSource": "exact"
},
@@ -101265,6 +101568,7 @@
"tests/corpus/5835-util-format-integer-specifiers.ts",
"tests/corpus/5884-inspect-layout-options.ts",
"tests/corpus/6428-promisify-callback-functions.ts",
"tests/corpus/7747-text-codec-untyped-slot.js",
"tests/corpus/node24-deep-equality-properties.js",
"tests/corpus/node24-deep-equality.cjs",
"tests/corpus/node24-deep-equality.ts",
@@ -101793,7 +102097,8 @@
"tests/corpus/text-codec-values/main.ts",
"tests/fixtures/node-types/text-codecs.mts",
"tests/harness/project-config.test.ts",
"tests/corpus/3133-text-decoder-streaming.ts"
"tests/corpus/3133-text-decoder-streaming.ts",
"tests/corpus/7747-text-codec-untyped-slot.js"
]
},
"anchorSource": "exact"
@@ -101914,7 +102219,8 @@
"tests/corpus/text-codec-values/main.ts",
"tests/fixtures/node-types/text-codecs.mts",
"tests/harness/project-config.test.ts",
"tests/corpus/3133-text-decoder-streaming.ts"
"tests/corpus/3133-text-decoder-streaming.ts",
"tests/corpus/7747-text-codec-untyped-slot.js"
]
},
"anchorSource": "exact"
@@ -102024,7 +102330,8 @@
"tests/corpus/text-codec-values/main.ts",
"tests/fixtures/node-types/text-codecs.mts",
"tests/harness/project-config.test.ts",
"tests/corpus/3133-text-decoder-streaming.ts"
"tests/corpus/3133-text-decoder-streaming.ts",
"tests/corpus/7747-text-codec-untyped-slot.js"
]
},
"anchorSource": "exact"
@@ -102053,7 +102360,8 @@
"tests/corpus/text-codec-values/main.ts",
"tests/fixtures/node-types/text-codecs.mts",
"tests/harness/project-config.test.ts",
"tests/corpus/3133-text-decoder-streaming.ts"
"tests/corpus/3133-text-decoder-streaming.ts",
"tests/corpus/7747-text-codec-untyped-slot.js"
]
},
"anchorSource": "exact"
@@ -111471,6 +111779,7 @@
"tests/corpus/2630-stream-consumers-errors.ts",
"tests/corpus/2862-web-readable-async-iteration.ts",
"tests/corpus/4875-stream-consumers-array-buffer.ts",
"tests/corpus/7743-transform-stream-identity.ts",
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
"tests/harness/fetch-conformance.test.ts"
@@ -112945,8 +113254,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webstreams"
"status": "partial",
"evidence": "compiler-feature:webstreams.TransformStream",
"tests": [
"tests/corpus/7743-transform-stream-identity.ts"
]
},
"anchorSource": "exact"
},
@@ -115600,7 +115912,10 @@
"tests/corpus/5377-zlib-stream-backpressure.cjs",
"tests/corpus/5378-zlib-sync-options.cjs",
"tests/corpus/5379-zlib-callback-options.ts",
"tests/corpus/5415-zlib-dynamic-inputs.cjs"
"tests/corpus/5415-zlib-dynamic-inputs.cjs",
"tests/corpus/7714-zlib-brotli-compress.cjs",
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "chapter"
@@ -116083,8 +116398,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliCompress"
"status": "partial",
"evidence": "compiler-feature:zlib.zlib.BrotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -116106,8 +116424,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliDecompress"
"status": "partial",
"evidence": "compiler-feature:zlib.zlib.BrotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -116625,8 +116947,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliCompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.createBrotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -116648,8 +116973,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliDecompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.createBrotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -116955,8 +117284,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -116978,8 +117310,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -117001,8 +117336,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -117024,8 +117363,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "exact"
},
@@ -118042,8 +118385,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:zlib"
"status": "partial",
"evidence": "compiler-feature:zlib.BrotliOptions",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "exact"
},
@@ -118111,8 +118457,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompress",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -118134,8 +118483,11 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliCompressSync",
"tests": [
"tests/corpus/7714-zlib-brotli-compress.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -118157,8 +118509,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompress",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "ancestor"
},
@@ -118180,8 +118536,12 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompressSync"
"status": "partial",
"evidence": "surface-manifest:node-builtin.zlib.brotliDecompressSync",
"tests": [
"tests/corpus/7715-zlib-brotli-decompress.cjs",
"tests/corpus/7716-zlib-brotli.ts"
]
},
"anchorSource": "ancestor"
},
+8 -1
View File
@@ -652,7 +652,14 @@ function publicDetail(tier) {
source,
)
) {
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.";
return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Zstd streams remain unsupported.";
}
if (
/^compiler-feature:zlib\.(?:zlib\.(?:BrotliCompress|BrotliDecompress)|BrotliOptions)$/.test(
source,
)
) {
return "BrotliCompress and BrotliDecompress are Transform streams over the brotli version Node bundles, with Node's parameters, dictionaries, flush, reset, error codes, and option validation; like Node's, they have no params().";
}
if (source === "compiler-feature:zlib.zlib.constants") {
return "Each zlib.constants member and its module-level copy reads as Node's value; the constants object itself is not a runtime value.";
+26 -6
View File
@@ -44,8 +44,8 @@
"node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"],
"node-builtin.crypto.checkPrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
"node-builtin.crypto.checkPrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
"node-builtin.crypto.createCipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createDecipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createCipheriv": ["tests/corpus/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createDecipheriv": ["tests/corpus/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"],
"node-builtin.crypto.createDiffieHellmanGroup": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"],
"node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"],
@@ -124,6 +124,7 @@
"node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"],
"node-builtin.crypto.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
"node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
"node-builtin.crypto.getCipherInfo": ["tests/corpus/7154-crypto-cipher-info.cjs", "tests/corpus/7157-crypto-cipher-info-typed.ts"],
"node-builtin.crypto.getDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"],
"node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"],
"node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
@@ -189,6 +190,12 @@
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"],
"node-builtin.zlib.brotliCompressSync": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
"node-builtin.zlib.brotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
"node-builtin.zlib.createBrotliCompress": ["tests/corpus/7714-zlib-brotli-compress.cjs"],
"node-builtin.zlib.brotliDecompressSync": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
"node-builtin.zlib.brotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
"node-builtin.zlib.createBrotliDecompress": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"],
"stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.map": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/537-map-iter-drains.ts"],
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
@@ -201,6 +208,8 @@
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
{ "chapter": "tls", "parent": "Class: `tls.TLSSocket`", "signatures": ["`authorized` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
{ "chapter": "crypto", "parent": "Class: `X509Certificate`", "signatures": ["`publicKey` Type: {KeyObject}"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
@@ -331,11 +340,11 @@
{ "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] },
{ "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" },
{ "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] },
{ "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts"] },
{ "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] },
{ "chapter": "globals", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5287-blob-inspect.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js"] },
{ "chapter": "buffer", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5288-blob-validation.js", "tests/fixtures/node-types/blob-file.ts"] },
{ "chapter": "buffer", "signatures": ["`blob.arrayBuffer()`", "`blob.bytes()`", "`blob.text()`", "`blob.slice([start[, end[, type]]])`", "`blob.size`", "`type` Type: {string}", "`name` Type: {string}", "`lastModified` Type: {number}"], "status": "supported", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5289-blob-read-order.ts", "tests/corpus/5290-blob-dynamic.js", "tests/fixtures/node-types/blob-file.ts"] },
{ "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts"] },
{ "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] },
{ "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs", "tests/corpus/2923-cjs-factory-export/main.cjs"] },
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] },
{ "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] },
@@ -403,10 +412,15 @@
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
{ "chapter": "errors", "symbols": ["ERR_BROTLI_INVALID_PARAM"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.BrotliDecompress`"], "status": "partial", "evidence": ["tests/corpus/7715-zlib-brotli-decompress.cjs", "tests/corpus/7716-zlib-brotli.ts"] },
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
{ "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] }
{ "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] },
{ "chapter": "webstreams", "symbols": ["TransformStream"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] },
{ "chapter": "globals", "signatures": ["Class: `TransformStream`"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] }
],
"dedicated": [
{ "symbols": ["assert.doesNotThrow", "assert.doesNotReject"], "status": "partial", "evidence": ["tests/corpus/node24-no-error-assertions.ts", "tests/corpus/node24-no-error-assertions.cjs", "tests/corpus/node24-no-error-assertions-coercion.js", "tests/fixtures/node-types/no-error-assertions.ts", "tests/harness/project-config.test.ts"] },
@@ -424,6 +438,11 @@
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] },
{ "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] },
{ "chapter": "tls", "symbols": ["server.addContext", "server.setSecureContext", "'tlsClientError'"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/7441-tls-client-error.ts", "tests/corpus/7442-tls-server-contexts.mjs", "tests/fixtures/node-types/tls-server-contexts.ts"] },
{ "chapter": "errors", "symbols": ["ERR_TLS_REQUIRED_SERVER_NAME"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts"] },
{ "chapter": "tls", "symbols": ["tlsSocket.authorizationError"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
{ "chapter": "tls", "symbols": ["tlsSocket.getPeerCertificate", "tlsSocket.getCertificate", "tlsSocket.getPeerX509Certificate", "tlsSocket.getX509Certificate"], "status": "partial", "evidence": ["tests/corpus/7095-tls-peer-certificates.ts", "tests/corpus/7096-tls-peer-certificates.mjs", "tests/fixtures/node-types/tls-peer-certificates.ts"] },
{ "chapter": "crypto", "symbols": ["x509.checkEmail", "x509.checkHost", "x509.checkIP", "x509.checkIssued", "x509.checkPrivateKey", "x509.toJSON", "x509.toLegacyObject", "x509.toString", "x509.verify"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
{ "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] },
{ "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"] },
{ "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] },
@@ -434,11 +453,12 @@
{ "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] },
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["X509Certificate"], "status": "partial", "evidence": ["tests/corpus/1568-x509-fingerprint.ts", "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/6936-x509-certificate-fields.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.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", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", "tests/corpus/7385-als-child-process-events.ts", "tests/corpus/7386-als-child-fork-events/main.mjs", "tests/corpus/7387-als-http2-events.mjs", "tests/corpus/7388-als-http2-compat-events.mjs", "tests/corpus/7389-als-http2-secure-events.mjs", "tests/corpus/7391-als-response-end-and-finished.mjs"] },
{ "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"] },
+165 -9
View File
@@ -19,6 +19,7 @@
#include "llvm/CodeGen/MachineFunction.h"
#include "llvm/CodeGen/MachineFunctionPass.h"
#include "llvm/CodeGen/MachineModuleInfo.h"
#include "llvm/IR/DiagnosticHandler.h"
#include "llvm/IR/DiagnosticInfo.h"
#include "llvm/IR/DiagnosticPrinter.h"
#include "llvm/IR/LegacyPassManager.h"
@@ -41,11 +42,13 @@
#include "llvm/Support/SourceMgr.h"
#include "llvm/Support/ThreadPool.h"
#include "llvm/Support/Threading.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Target/TargetMachine.h"
#include "llvm/TargetParser/Triple.h"
#include "llvm/Transforms/Utils/SplitModule.h"
#include <atomic>
#include <mutex>
#include <optional>
#include <string>
@@ -170,6 +173,108 @@ void limitProfileImports() {
(void)Limited;
}
// How the --profile-use profile matched the functions it annotated, summed
// over partitions: functions that received its counts, functions whose
// control flow changed since it was collected (hash mismatch), and functions
// it has no record of (new, or never run in training: the merged profile is
// sparse). A partition reused from the cache without its counts is counted
// in Unknown.
struct ProfileMatch {
std::atomic<uint64_t> Annotated{0};
std::atomic<uint64_t> Mismatched{0};
std::atomic<uint64_t> Missing{0};
std::atomic<uint64_t> Unknown{0};
void add(const ProfileMatch &Other) {
Annotated += Other.Annotated;
Mismatched += Other.Mismatched;
Missing += Other.Missing;
}
std::string serialize() const {
return formatv("{0} {1} {2}", Annotated.load(), Mismatched.load(),
Missing.load())
.str();
}
bool deserialize(StringRef Text) {
SmallVector<StringRef, 3> Parts;
Text.split(Parts, ' ');
uint64_t Values[3];
if (Parts.size() != 3)
return false;
for (size_t I = 0; I < 3; ++I)
if (Parts[I].getAsInteger(10, Values[I]))
return false;
Annotated += Values[0];
Mismatched += Values[1];
Missing += Values[2];
return true;
}
};
// Counts the profile annotation's hash-mismatch and missing-function
// warnings instead of printing them. Every other diagnostic takes the
// context's default handling.
class ProfileMatchHandler : public DiagnosticHandler {
public:
explicit ProfileMatchHandler(ProfileMatch &Match) : Match(Match) {}
bool handleDiagnostics(const DiagnosticInfo &Info) override {
if (Info.getKind() != DK_PGOProfile)
return false;
std::string Message;
raw_string_ostream Stream(Message);
DiagnosticPrinterRawOStream Printer(Stream);
Info.print(Printer);
if (StringRef(Message).contains("hash mismatch"))
++Match.Mismatched;
else if (StringRef(Message).contains_insensitive("no profile data"))
++Match.Missing;
else
return false;
return true;
}
private:
ProfileMatch &Match;
};
// The annotation warns about functions the profile does not describe only
// when asked; the match counts need those warnings. Process-wide, set once.
void reportMissingProfiles() {
static const bool Reported = [] {
if (cl::Option *Option =
cl::getRegisteredOptions().lookup("pgo-warn-missing-function"))
Option->addOccurrence(0, "pgo-warn-missing-function", "true");
return true;
}();
(void)Reported;
}
// Functions of `M` that the profile annotated with counts.
uint64_t annotatedFunctions(const Module &M) {
uint64_t Count = 0;
for (const Function &F : M) {
if (F.isDeclaration())
continue;
std::optional<Function::ProfileCount> Entry = F.getEntryCount();
if (Entry && !Entry->isSynthetic())
++Count;
}
return Count;
}
// Prints the profile match as the successful emit's one line of JSON.
void reportProfileMatch(const ProfileMatch &Match) {
json::Object Profile{{"annotated", Match.Annotated.load()},
{"mismatched", Match.Mismatched.load()},
{"missing", Match.Missing.load()},
{"unknown_partitions", Match.Unknown.load()}};
json::Object Response{{"ok", true}, {"profile", std::move(Profile)}};
outs() << formatv("{0}\n", json::Value(std::move(Response)));
}
// Reads the profile's header once, before any partition needs it: it must
// hold IR-level counts.
std::optional<EmitFailure> inspectProfile(const EmitOptions &Options) {
@@ -191,6 +296,7 @@ std::optional<EmitFailure> inspectProfile(const EmitOptions &Options) {
// Owns the analysis managers a pipeline needs for one module.
struct Pipeline {
PassInstrumentationCallbacks Callbacks;
LoopAnalysisManager LAM;
FunctionAnalysisManager FAM;
CGSCCAnalysisManager CGAM;
@@ -199,13 +305,26 @@ struct Pipeline {
Pipeline(TargetMachine &Machine, OptimizationLevel Level,
std::optional<PGOOptions> Profile = std::nullopt)
: PB(&Machine, tuningFor(Level), std::move(Profile)) {
: PB(&Machine, tuningFor(Level), std::move(Profile), &Callbacks) {
PB.registerModuleAnalyses(MAM);
PB.registerCGSCCAnalyses(CGAM);
PB.registerFunctionAnalyses(FAM);
PB.registerLoopAnalyses(LAM);
PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);
}
// Adds to Count the functions the profile annotation gave counts to, as
// soon as that pass has run: inlining and dead code elimination later in
// the pipeline delete annotated functions.
void countAnnotated(std::atomic<uint64_t> &Count) {
Callbacks.registerAfterPassCallback(
[&Count](StringRef PassID, Any IR, const PreservedAnalyses &) {
if (PassID != "PGOInstrumentationUse")
return;
if (const auto *M = llvm::any_cast<const Module *>(&IR))
Count += annotatedFunctions(**M);
});
}
};
std::optional<EmitFailure> verify(Module &M, StringRef Code) {
@@ -387,8 +506,15 @@ std::optional<EmitFailure> publish(StringRef TemporaryPath,
std::optional<EmitFailure>
emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level,
std::optional<PGOOptions> Profile, CodeGenFileType Type,
StringRef OutputPath, std::vector<SmallString<256>> &Temporaries) {
StringRef OutputPath, std::vector<SmallString<256>> &Temporaries,
ProfileMatch &Match) {
bool Annotating = Profile && Profile->Action == PGOOptions::IRUse;
if (Annotating)
M.getContext().setDiagnosticHandler(
std::make_unique<ProfileMatchHandler>(Match));
Pipeline P(Machine, Level, std::move(Profile));
if (Annotating)
P.countAnnotated(Match.Annotated);
P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM);
if (std::optional<EmitFailure> Failure =
verify(M, "post_optimization_verification_failed"))
@@ -555,20 +681,32 @@ std::optional<EmitFailure>
compileOptimizedPartitions(const EmitOptions &Options,
const PartitionPlan &Plan, OptimizationLevel Level,
StringRef Salt, const ArtifactCache &Cache,
std::vector<SmallVector<char, 0>> &Objects) {
std::vector<SmallVector<char, 0>> &Objects,
ProfileMatch &Match) {
size_t Count = Plan.Modules.size();
bool Annotating = !Options.ProfileUse.empty();
std::vector<std::unique_ptr<MemoryBuffer>> Summaries(Count);
SharedFailure Failures;
forEachPartition(Count, [&](size_t I) {
std::string Key = hashParts({Salt, "summary", Plan.Modules[I]});
if ((Summaries[I] = Cache.lookup(Key, "bc")))
if ((Summaries[I] = Cache.lookup(Key, "bc"))) {
if (Annotating) {
std::unique_ptr<MemoryBuffer> Counts = Cache.lookup(Key, "match");
if (!Counts || !Match.deserialize(Counts->getBuffer()))
++Match.Unknown;
}
return;
}
std::string LookupError;
std::unique_ptr<TargetMachine> Machine =
createTargetMachine(Options.Target, Options.OptLevel, LookupError);
if (!Machine)
return Failures.set({"target_machine_failed", LookupError});
LLVMContext Context;
ProfileMatch PartitionMatch;
if (Annotating)
Context.setDiagnosticHandler(
std::make_unique<ProfileMatchHandler>(PartitionMatch));
Expected<std::unique_ptr<Module>> Loaded =
loadPartition(Options, Plan.Modules[I], I, Context, *Machine);
if (!Loaded)
@@ -592,12 +730,19 @@ compileOptimizedPartitions(const EmitOptions &Options,
// Profile instrumentation or annotation happens here, before the thin
// link; the post-link optimization reads the annotated weights.
Pipeline P(*Machine, Level, profileOptions(Options));
if (Annotating)
P.countAnnotated(PartitionMatch.Annotated);
ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level);
MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false,
/*EmitSummaryIndex=*/true,
/*EmitModuleHash=*/true));
MPM.run(Mod, P.MAM);
}
if (Annotating) {
Match.add(PartitionMatch);
// Stored before the summary, so a reused summary finds its counts.
Cache.store(Key, "match", PartitionMatch.serialize());
}
StringRef Contents(Bitcode.data(), Bitcode.size());
Cache.store(Key, "bc", Contents);
Summaries[I] = MemoryBuffer::getMemBufferCopy(Contents);
@@ -744,7 +889,8 @@ compileOptimizedPartitions(const EmitOptions &Options,
std::optional<EmitFailure>
compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan,
std::vector<SmallVector<char, 0>> &Objects) {
std::vector<SmallVector<char, 0>> &Objects,
ProfileMatch &Match) {
// Everything besides a partition's own bytes that determines its
// artifacts. The caller's cache directory already identifies the exact
// helper build; source content and partition count arrive per module.
@@ -778,7 +924,7 @@ compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan,
return compileUnoptimizedPartitions(Options, Plan, Salt, Cache, Objects);
return compileOptimizedPartitions(Options, Plan,
optimizationLevel(Options.OptLevel), Salt,
Cache, Objects);
Cache, Objects, Match);
}
// Publishes in-memory partition objects through private sibling files.
@@ -819,6 +965,8 @@ int emitPartitions(const EmitOptions &Options) {
std::vector<SmallVector<char, 0>> Objects;
std::optional<EmitFailure> Failure;
bool Compiled = false;
// A failed attempt's counts are discarded with its objects.
auto Match = std::make_unique<ProfileMatch>();
{
ErrorOr<std::unique_ptr<MemoryBuffer>> Input =
MemoryBuffer::getFile(Options.Input, /*IsText=*/false,
@@ -830,15 +978,16 @@ int emitPartitions(const EmitOptions &Options) {
Input->reset();
PartitionPlan Plan{std::move(Text->Sources),
std::move(Text->PublicSymbols)};
Compiled = !compilePartitions(Options, Plan, Objects);
Compiled = !compilePartitions(Options, Plan, Objects, *Match);
}
}
}
if (!Compiled) {
Match = std::make_unique<ProfileMatch>();
PartitionPlan Plan;
Failure = splitParsedModule(Options, Plan);
if (!Failure)
Failure = compilePartitions(Options, Plan, Objects);
Failure = compilePartitions(Options, Plan, Objects, *Match);
}
std::vector<SmallString<256>> Temporaries;
if (!Failure)
@@ -851,6 +1000,8 @@ int emitPartitions(const EmitOptions &Options) {
return reportError(Failure->Code, Failure->Message,
Options.DiagnosticFormat);
}
if (!Options.ProfileUse.empty())
reportProfileMatch(*Match);
return 0;
}
@@ -895,6 +1046,8 @@ int emit(const EmitOptions &Options) {
return reportError("invalid_import",
"runtime bitcode import requires an optimized build",
Options.DiagnosticFormat);
if (!Options.ProfileUse.empty())
reportMissingProfiles();
if (std::optional<EmitFailure> Failure = inspectProfile(Options))
return reportError(Failure->Code, Failure->Message,
Options.DiagnosticFormat);
@@ -957,9 +1110,10 @@ int emit(const EmitOptions &Options) {
? CodeGenFileType::ObjectFile
: CodeGenFileType::AssemblyFile;
std::vector<SmallString<256>> Temporaries;
ProfileMatch Match;
std::optional<EmitFailure> Failure =
emitModule(*Mod, *Machine, Level, profileOptions(Options), Type,
Options.Outputs[0], Temporaries);
Options.Outputs[0], Temporaries, Match);
if (!Failure)
Failure = publish(Temporaries[0], Options.Outputs[0]);
if (Failure) {
@@ -968,6 +1122,8 @@ int emit(const EmitOptions &Options) {
return reportError(Failure->Code, Failure->Message,
Options.DiagnosticFormat);
}
if (!Options.ProfileUse.empty())
reportProfileMatch(Match);
return 0;
}
+94
View File
@@ -11,6 +11,7 @@
#include <algorithm>
#include <cstdint>
#include <string>
using namespace llvm;
@@ -36,6 +37,9 @@ struct Symbol {
std::string PromotedHeader;
std::vector<unsigned> Refs;
std::vector<unsigned> Metadata;
// The source-module group the emitter wrote before this definition, or
// empty.
StringRef Group;
unsigned Partition = NoPartition;
};
@@ -70,6 +74,83 @@ uint32_t stableHash(StringRef Name) {
return Hash;
}
// Places the functions whose root carries a source-module group by group: a
// module's functions call each other most, so keeping them together leaves
// fewer calls for the thin link to import across partitions. A group larger
// than the mean partition, or than MaxChunkBytes, is divided into a power of
// two of chunks by the hash of each root's name, so the division changes only
// when the group's size doubles. Chunks are placed largest first, each in the
// lighter of two partitions chosen by hashes of its key, after the functions
// placed by name. The placement depends only on the module text and the
// partition count. Modules with fewer than MinGroupedPartitions partitions
// keep the placement by name: with few partitions per core, the balance of
// the largest partitions decides the build time more than the imports.
void placeGroups(std::vector<Symbol> &Symbols, ArrayRef<unsigned> Roots,
unsigned Count) {
constexpr unsigned MinGroupedPartitions = 32;
if (Count < MinGroupedPartitions)
return;
auto Bytes = [](const Symbol &S) -> uint64_t {
return S.Header.size() + S.Body.size();
};
std::vector<uint64_t> Load(Count, 0);
uint64_t Total = 0;
StringMap<uint64_t> GroupBytes;
for (unsigned I = 0; I < Symbols.size(); ++I) {
const Symbol &S = Symbols[I];
if (S.Kind != SymbolKind::Function)
continue;
Total += Bytes(S);
StringRef Group = Symbols[Roots[I]].Group;
if (Group.empty())
Load[S.Partition] += Bytes(S);
else
GroupBytes[Group] += Bytes(S);
}
if (GroupBytes.empty())
return;
// A chunk is at most MaxChunkBytes, so the code a partition optimizes
// together, and the inlining within it, does not grow when a program has
// fewer partitions.
constexpr uint64_t MaxChunkBytes = 1024 * 1024;
uint64_t Chunk =
std::min(MaxChunkBytes, std::max<uint64_t>(1, Total / Count));
std::vector<std::string> Keys(Symbols.size());
StringMap<uint64_t> KeyBytes;
for (unsigned I = 0; I < Symbols.size(); ++I) {
const Symbol &S = Symbols[I];
StringRef Group = Symbols[Roots[I]].Group;
if (S.Kind != SymbolKind::Function || Group.empty())
continue;
uint64_t Chunks = 1;
while (Chunks * Chunk < GroupBytes[Group])
Chunks *= 2;
Keys[I] = Chunks == 1
? Group.str()
: (Group + "#" +
Twine(stableHash(Symbols[Roots[I]].Name) % Chunks))
.str();
KeyBytes[Keys[I]] += Bytes(S);
}
std::vector<std::pair<StringRef, uint64_t>> Order;
for (const auto &Entry : KeyBytes)
Order.push_back({Entry.getKey(), Entry.getValue()});
llvm::sort(Order, [](const auto &A, const auto &B) {
return A.second != B.second ? A.second > B.second : A.first < B.first;
});
StringMap<unsigned> Where;
for (const auto &[Key, Size] : Order) {
unsigned First = stableHash(Key) % Count;
unsigned Second = stableHash((Key + "/2").str()) % Count;
unsigned Chosen = Load[Second] < Load[First] ? Second : First;
Load[Chosen] += Size;
Where[Key] = Chosen;
}
for (unsigned I = 0; I < Symbols.size(); ++I)
if (!Keys[I].empty())
Symbols[I].Partition = Where[Keys[I]];
}
bool startsWithLinkage(StringRef Rest) {
for (StringRef Linkage :
{"linkonce", "weak", "common", "appending", "extern_weak",
@@ -326,6 +407,11 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
return true;
};
// The emitter writes "; scriptc-group <key>" before the definitions of each
// source function; the group applies until the next such line, and an
// empty key ends it.
constexpr StringRef GroupLine = "; scriptc-group";
StringRef Group;
size_t Pos = 0;
while (Pos < Text.size()) {
size_t End = Text.find('\n', Pos);
@@ -335,10 +421,15 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
size_t Next = End + 1;
if (Line.empty() || Line.trim().empty()) {
// Blank separator.
} else if (Line.starts_with(GroupLine) &&
(Line.size() == GroupLine.size() ||
Line[GroupLine.size()] == ' ')) {
Group = Line.drop_front(GroupLine.size()).trim();
} else if (Line.starts_with("define ")) {
Symbol S;
S.Kind = SymbolKind::Function;
S.Header = Line;
S.Group = Group;
if (!parseFunctionHeader(Line, S))
return std::nullopt;
// The emitter indents every body line, so the first line that is
@@ -466,6 +557,7 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
Referrer[Ref] = I;
}
}
std::vector<unsigned> Roots(Symbols.size());
for (unsigned I = 0; I < Symbols.size(); ++I) {
if (Symbols[I].Kind != SymbolKind::Function)
continue;
@@ -479,8 +571,10 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
break;
Root = Parent;
}
Roots[I] = Root;
Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count;
}
placeGroups(Symbols, Roots, Count);
for (const Symbol &S : Symbols) {
if (S.Kind != SymbolKind::Function)
continue;
+9 -5
View File
@@ -20,11 +20,15 @@ struct ProgramPartitions {
};
// Divides the LLVM assembly scriptc emits into Count modules without parsing
// it, so the partitions can be parsed concurrently. Functions are placed by a
// stable hash of their name; an internal function referenced by exactly one
// other function stays with that function unless its body is too large to
// inline, and a global stays with the first function that references it. Returns nullopt for any construct outside the
// emitter's narrow top-level grammar; callers then split a parsed module.
// it, so the partitions can be parsed concurrently. In modules of 32 or
// more partitions, functions under a "; scriptc-group <key>" line are placed
// with the other functions of that source-module group, which is divided
// into chunks when it is larger than the mean partition or 1 MiB; other
// functions are placed by a stable hash of their name. An internal function referenced by exactly one other function stays
// with that function unless its body is too large to inline, and a global
// stays with the first function that references it. Returns nullopt for any
// construct outside the emitter's narrow top-level grammar; callers then
// split a parsed module.
std::optional<ProgramPartitions> partitionProgramText(llvm::StringRef Text,
unsigned Count);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-arm64",
"version": "0.4.2",
"version": "0.4.4",
"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.4.2",
"version": "0.4.4",
"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.4.2",
"version": "0.4.4",
"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.4.2",
"version": "0.4.4",
"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.4.2",
"version": "0.4.4",
"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.4.2",
"version": "0.4.4",
"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.4.2",
"version": "0.4.4",
"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.4.2",
"version": "0.4.4",
"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",
+24 -2
View File
@@ -70,7 +70,11 @@ async function tryFastPath(): Promise<number | null> {
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
values.sparse ||
(values["external-types"] ?? []).length > 0
// The full compiler resolves `native` against the build host.
values["target-cpu"] !== undefined ||
(values["external-types"] ?? []).length > 0 ||
// The full CLI validates the Node island policy flags.
hasNodeIslandFlags(values)
)
return null;
const backend = values.backend;
@@ -180,6 +184,21 @@ async function tryFastPath(): Promise<number | null> {
});
}
function hasNodeIslandFlags(
values: ReturnType<
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
>["values"],
): boolean {
return (
values["node-islands"] !== undefined ||
(values["node-island"] ?? []).length > 0 ||
(values.native ?? []).length > 0 ||
values.node !== undefined ||
values["explain-island"] !== undefined ||
values["strict-user-js"]
);
}
/** An unchanged program's coverage verdict replays from the verdict cache
* without loading the compiler graph. Anything else (a miss, a comment-only
* edit, options the replay does not cover) defers to the full CLI. */
@@ -200,6 +219,7 @@ async function coverageFastPath(
values.emit !== undefined ||
values.strip ||
values["detached-exit"] ||
values["target-cpu"] !== undefined ||
values.lib ||
values.profile !== undefined ||
values.ffi !== undefined ||
@@ -209,7 +229,9 @@ async function coverageFastPath(
values["provenance-sources"] ||
values["emit-ir"] ||
(values["npm-static"] ?? []).length > 0 ||
(values["external-types"] ?? []).length > 0
(values["external-types"] ?? []).length > 0 ||
// The replay covers the default Node island policy only.
hasNodeIslandFlags(values)
)
return null;
let startup: typeof import("@scriptc/compiler/startup-cache");
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" },
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
() => null,
(failure: { stderr?: string }) => failure,
);
expect(error?.stderr).toContain("scr_runtime_abi_v36");
expect(error?.stderr).toContain("scr_runtime_abi_v44");
});
});
+100
View File
@@ -0,0 +1,100 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { hostTargetCpu } from "../../compiler/src/backend/target-cpu.js";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const repoRoot = join(import.meta.dirname, "../../..");
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js");
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
const host = hostTargetCpu();
const runsV3 = host === "x86-64-v3" || host === "x86-64-v4";
const program = `const a = new Int32Array(4096);
for (let i = 0; i < a.length; i++) a[i] = (i * 2654435761) | 0;
let s = 0;
for (let r = 0; r < 64; r++) for (let i = 0; i < a.length; i++) s = (s + a[i]!) | 0;
console.log(s);
`;
test.runIf(process.platform === "linux" && process.arch === "x64")(
"--target-cpu compiles the program for the level without mixing cache variants",
async () => {
const dir = await mkdtemp(join("/tmp", "scriptc-target-cpu-"));
const entry = join(dir, "main.ts");
const outPath = join(dir, "program");
const llvmPath = join(dir, "main.ll");
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
const build = async (args: string[], bootstrap = false): Promise<string> => {
await execFileAsync(
process.execPath,
[
...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]),
"build",
entry,
"-o",
outPath,
...args,
],
{ env, maxBuffer: 4 * 1024 * 1024 },
);
return readFile(llvmPath, "utf8");
};
try {
await writeFile(entry, program);
const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
const baseline = await build([]);
expect(baseline).not.toContain('"target-cpu"');
expect(await build(["--target-cpu=baseline"])).toBe(baseline);
const v3 = await build(["--target-cpu=x86-64-v3"]);
expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected);
// The bootstrap's cached route builds without a level.
expect(await build([], true)).toBe(baseline);
expect((await execFileAsync(outPath)).stdout).toBe(expected);
await expect(build(["--target-cpu=haswell"])).rejects.toThrow(/unknown target CPU "haswell"/);
} finally {
await rm(dir, { recursive: true, force: true });
}
},
240_000,
);
test("--target-cpu levels are refused for arm64 and WASI targets", async () => {
const dir = await mkdtemp(join("/tmp", "scriptc-target-cpu-refusal-"));
const entry = join(dir, "main.ts");
try {
await writeFile(entry, program);
for (const target of ["aarch64-unknown-linux-gnu", "wasm32-wasi"]) {
for (const kind of ["llvm", "asm", "obj", "exe"]) {
const build = execFileAsync(
process.execPath,
[
"--import",
tsxLoader,
cliEntry,
"build",
entry,
`--emit=${kind}`,
"-o",
join(dir, `out-${kind}`),
"--target-cpu=x86-64-v3",
],
{
env: { ...process.env, SCRIPTC_TARGET: target, SCRIPTC_CACHE_DIR: join(dir, "cache") },
},
);
await expect(build, `${target} ${kind}`).rejects.toThrow(
/--target-cpu=x86-64-v3 requires an x86-64 target/,
);
}
}
} finally {
await rm(dir, { recursive: true, force: true });
}
}, 240_000);
+452 -73
View File
@@ -917,6 +917,14 @@ interface Buffer<
readBigUint64LE(offset?: number): bigint;
}
interface BufferConstructor {
/** @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. */
new (str: string, encoding?: BufferEncoding): Buffer;
/** @deprecated since v10.0.0 - Use `Buffer.alloc()` instead. */
new (size: number): Buffer;
/** @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. */
new (array: ArrayLike<number>): Buffer;
/** @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. */
new (arrayBuffer: ArrayBuffer | SharedArrayBuffer): Buffer;
from(data: string, encoding?: BufferEncoding): Buffer;
/* The ArrayBuffer form is the VIEW construction: x.buffer with an
* optional byte offset/length shares x's storage (the subarray rule). */
@@ -1106,6 +1114,34 @@ declare var ReadableStream: {
from(iterable: string): ReadableStream<string>;
readonly prototype: ReadableStream<unknown>;
};
/* The writable side of a TransformStream and its writer. The
* WritableStream constructor has no native lowering; writable streams come
* from `new TransformStream()`. */
interface WritableStreamDefaultWriter<W = unknown> {
readonly closed: Promise<void>;
readonly desiredSize: number | null;
readonly ready: Promise<void>;
abort(reason?: unknown): Promise<void>;
close(): Promise<void>;
releaseLock(): void;
write(chunk?: W): Promise<void>;
}
interface WritableStream<W = unknown> {
readonly locked: boolean;
abort(reason?: unknown): Promise<void>;
close(): Promise<void>;
getWriter(): WritableStreamDefaultWriter<W>;
}
/* The identity transform: chunks written to `writable` are read from
* `readable`, and a write completes when its chunk is read. */
interface TransformStream<I = unknown, O = unknown> {
readonly readable: ReadableStream<O>;
readonly writable: WritableStream<I>;
}
declare var TransformStream: {
new <I = unknown, O = I>(): TransformStream<I, O>;
readonly prototype: TransformStream<unknown, unknown>;
};
/* fetch, typed as the native JSON + readable-stream slice: one URL, an
* optional init with the members CLI requests carry, and a Response
@@ -1554,7 +1590,7 @@ declare module "node:fs" {
export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void;
export function writeFileSync(path: string | URL, data: string | Uint8Array): void;
export function appendFileSync(path: string | URL, data: string | Uint8Array): void;
export function existsSync(path: string): boolean;
export function existsSync(path: string | URL): boolean;
export function mkdirSync(path: string | URL): void;
export function mkdirSync(
path: string | URL,
@@ -2195,7 +2231,7 @@ declare module "fs/promises" {
stat(): Promise<import("node:fs").Stats>;
[Symbol.asyncDispose](): Promise<void>;
}
export function open(path: string, flags?: string, mode?: number): Promise<FileHandle>;
export function open(path: string | URL, flags?: string, mode?: number): Promise<FileHandle>;
export function link(
existingPath: string | Uint8Array | URL,
newPath: string | Uint8Array | URL,
@@ -2232,14 +2268,14 @@ declare module "fs/promises" {
atime: number | string | Date,
mtime: number | string | Date,
): Promise<void>;
export function readFile(path: string, encoding: "utf8" | "utf-8"): Promise<string>;
export function readFile(path: string | URL, encoding: "utf8" | "utf-8"): Promise<string>;
export function readFile(
path: string,
path: string | URL,
options: BufferEncoding | { encoding: BufferEncoding },
): Promise<string>;
export function readFile(path: string): Promise<Buffer>;
export function readFile(path: string | URL): Promise<Buffer>;
export function writeFile(
path: string,
path: string | URL,
data: string,
options?: {
mode?: number;
@@ -2248,14 +2284,18 @@ declare module "fs/promises" {
[option: string]: unknown;
},
): Promise<void>;
export function writeFile(path: string, data: string, encoding: "utf8" | "utf-8"): Promise<void>;
export function writeFile(
path: string | URL,
data: string,
encoding: "utf8" | "utf-8",
): Promise<void>;
export function mkdir(
path: string,
path: string | URL,
options?: { recursive?: boolean; mode?: number },
): Promise<void>;
export function readdir(path: string): Promise<string[]>;
export function readdir(path: string | URL): Promise<string[]>;
export function readdir(
path: string,
path: string | URL,
options: {
encoding?: "utf8" | "utf-8";
withFileTypes: true;
@@ -2272,12 +2312,12 @@ declare module "fs/promises" {
pattern: string | readonly string[],
options: import("node:fs").GlobOptionsWithFileTypes,
): AsyncIterableIterator<import("node:fs").Dirent>;
export function rm(path: string): Promise<void>;
export function rm(path: string | URL): Promise<void>;
export function rm(
path: string,
path: string | URL,
options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number },
): Promise<void>;
export function stat(path: string): Promise<import("node:fs").Stats>;
export function stat(path: string | URL): Promise<import("node:fs").Stats>;
export function statfs(
path: string | Uint8Array | URL,
options?: import("node:fs").StatFsOptions & { bigint?: false },
@@ -2290,21 +2330,21 @@ declare module "fs/promises" {
path: string | Uint8Array | URL,
options?: import("node:fs").StatFsOptions,
): Promise<import("node:fs").StatsFs | import("node:fs").BigIntStatsFs>;
export function realpath(path: string): Promise<string>;
export function lstat(path: string): Promise<import("node:fs").Stats>;
export function unlink(path: string): Promise<void>;
export function chmod(path: string, mode: number): Promise<void>;
export function chown(path: string, uid: number, gid: number): Promise<void>;
export function lchown(path: string, uid: number, gid: number): Promise<void>;
export function access(path: string, mode?: number): Promise<void>;
export function truncate(path: string, len?: number): Promise<void>;
export function appendFile(path: string, data: string | Uint8Array): Promise<void>;
export function copyFile(src: string, dest: string): Promise<void>;
export function mkdtemp(prefix: string): Promise<string>;
export function rmdir(path: string): Promise<void>;
export function rename(oldPath: string, newPath: string): Promise<void>;
export function realpath(path: string | URL): Promise<string>;
export function lstat(path: string | URL): Promise<import("node:fs").Stats>;
export function unlink(path: string | URL): Promise<void>;
export function chmod(path: string | URL, mode: number): Promise<void>;
export function chown(path: string | URL, uid: number, gid: number): Promise<void>;
export function lchown(path: string | URL, uid: number, gid: number): Promise<void>;
export function access(path: string | URL, mode?: number): Promise<void>;
export function truncate(path: string | URL, len?: number): Promise<void>;
export function appendFile(path: string | URL, data: string | Uint8Array): Promise<void>;
export function copyFile(src: string | URL, dest: string | URL): Promise<void>;
export function mkdtemp(prefix: string | URL): Promise<string>;
export function rmdir(path: string | URL): Promise<void>;
export function rename(oldPath: string | URL, newPath: string | URL): Promise<void>;
export function opendir(
path: string,
path: string | URL,
options?: import("node:fs").OpenDirOptions,
): Promise<import("node:fs").Dir>;
}
@@ -2756,8 +2796,9 @@ declare var URLSearchParams: {
* slashes, non-empty host); pathToFileURL resolves the path and percent-
* encodes it into a file: URL. The legacy API: parse() answers a Url
* object, format() serializes a Url-shaped object, a string, or a URL,
* resolve() resolves one URL string against another, and the domain pair
* converts a hostname to and from its Punycode form. */
* resolve() resolves one URL string against another, the domain pair
* converts a hostname to and from its Punycode form, and urlToHttpOptions
* builds http.request options from a URL. */
declare module "url" {
export type URL = globalThis.URL;
export type URLSearchParams = globalThis.URLSearchParams;
@@ -2825,6 +2866,19 @@ declare module "url" {
export function resolve(from: string, to: string): string;
export function domainToASCII(domain: string): string;
export function domainToUnicode(domain: string): string;
/** urlToHttpOptions's null-prototype request options. */
export interface UrlHttpOptions {
protocol: string;
hostname: string;
hash: string;
search: string;
pathname: string;
path: string;
href: string;
port?: number;
auth?: string;
}
export function urlToHttpOptions(url: URL): UrlHttpOptions;
}
declare module "node:url" {
export * from "url";
@@ -2964,6 +3018,24 @@ declare module "crypto" {
options: ED25519KeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
export interface X25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
publicKeyEncoding: { type: "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
}
export function generateKeyPairSync(
type: "x25519",
options: X25519KeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "x25519",
options: X25519KeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(type: "x25519", options?: {}): KeyPairKeyObjectResult;
export function generateKeyPair(
type: "x25519",
options: {} | undefined,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export function generateKeyPair(
type: "ed25519",
options: {} | undefined,
@@ -2992,6 +3064,11 @@ declare module "crypto" {
export interface VerifyKeyObjectInput extends SigningOptions {
key: KeyObject;
}
export function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): Buffer;
export function diffieHellman(
options: { privateKey: KeyObject; publicKey: KeyObject },
callback: (err: Error | null, secret: Buffer) => void,
): void;
export function sign(
algorithm: string | null | undefined,
data: ArrayBufferView,
@@ -3171,13 +3248,17 @@ declare module "crypto" {
callback: (error: Error | null, buffer: Buffer) => void,
): void;
export type BinaryLike = string | ArrayBuffer | ArrayBufferView;
/* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8,
* cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and
* chacha20-poly1305. Both are Transforms: written chunks go through
* update(), and the end pushes final(). */
/* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ccm, ecb, cfb,
* cfb8, cfb1, and ofb modes and their key wraps, des-ede3 and des-ede,
* chacha20, and chacha20-poly1305. Both are Transforms: written chunks go
* through update(), and the end pushes final(). */
export type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm";
export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm";
export type CipherChaCha20Poly1305Types = "chacha20-poly1305";
export type CipherKey = BinaryLike | KeyObject;
export interface CipherCCMOptions {
authTagLength: number;
}
export interface CipherGCMOptions {
authTagLength?: number;
}
@@ -3197,6 +3278,10 @@ declare module "crypto" {
final(outputEncoding: BufferEncoding): string;
setAutoPadding(autoPadding?: boolean): this;
}
export interface CipherCCM extends Cipheriv {
setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this;
getAuthTag(): Buffer;
}
export interface CipherGCM extends Cipheriv {
setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this;
getAuthTag(): Buffer;
@@ -3218,6 +3303,10 @@ declare module "crypto" {
final(outputEncoding: BufferEncoding): string;
setAutoPadding(autoPadding?: boolean): this;
}
export interface DecipherCCM extends Decipheriv {
setAuthTag(buffer: ArrayBufferView): this;
setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this;
}
export interface DecipherGCM extends Decipheriv {
setAuthTag(buffer: ArrayBufferView): this;
setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this;
@@ -3226,6 +3315,12 @@ declare module "crypto" {
setAuthTag(buffer: ArrayBufferView): this;
setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this;
}
export function createCipheriv(
algorithm: CipherCCMTypes,
key: CipherKey,
iv: BinaryLike,
options: CipherCCMOptions,
): CipherCCM;
export function createCipheriv(
algorithm: CipherGCMTypes,
key: CipherKey,
@@ -3244,6 +3339,12 @@ declare module "crypto" {
iv: BinaryLike | null,
options?: { authTagLength?: number },
): Cipheriv;
export function createDecipheriv(
algorithm: CipherCCMTypes,
key: CipherKey,
iv: BinaryLike,
options: CipherCCMOptions,
): DecipherCCM;
export function createDecipheriv(
algorithm: CipherGCMTypes,
key: CipherKey,
@@ -3450,17 +3551,45 @@ declare module "crypto" {
keylen: number,
callback: (error: Error | null, derivedKey: ArrayBuffer) => void,
): void;
/* The lowered X509Certificate surface is the data-record slice:
* fingerprint (the SHA-1 of the DER, uppercase colon-separated) and
* the validFrom/validTo validity window (Node's ASN1_TIME_print
* strings — "Jul 1 00:00:00 2026 GMT"; the cert-expiry idiom composes
* them with new Date(...).getTime()). All compute at construction;
* unparseable input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */
/* The lowered X509Certificate surface is the data-record slice: the
* fields below, all computed at construction (raw decodes the input,
* and the others read raw). validFrom/validTo are Node's
* ASN1_TIME_print strings ("Jul 1 00:00:00 2026 GMT"). Unparseable
* input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */
export class X509Certificate {
constructor(buffer: Uint8Array | string);
readonly ca: boolean;
readonly fingerprint: string;
readonly fingerprint256: string;
readonly fingerprint512: string;
readonly subject: string;
readonly subjectAltName: string | undefined;
readonly infoAccess: string | undefined;
readonly issuer: string;
readonly raw: Buffer;
readonly serialNumber: string;
readonly signatureAlgorithm: string | undefined;
readonly signatureAlgorithmOid: string;
readonly validFrom: string;
readonly validFromDate: Date;
readonly validTo: string;
readonly validToDate: Date;
/* the certificate's public key; every read answers the same KeyObject */
readonly publicKey: KeyObject;
verify(publicKey: KeyObject): boolean;
checkPrivateKey(privateKey: KeyObject): boolean;
checkIssued(otherCert: X509Certificate): boolean;
checkHost(name: string, options?: X509CheckOptions): string | undefined;
checkEmail(email: string, options?: X509CheckOptions): string | undefined;
checkIP(ip: string): string | undefined;
toLegacyObject(): import("tls").PeerCertificate;
}
export interface X509CheckOptions {
subject?: "always" | "default" | "never";
wildcards?: boolean;
partialWildcards?: boolean;
multiLabelWildcards?: boolean;
singleLabelSubdomains?: boolean;
}
/* The crypto introspection statics: getFips() answers 0 (a compiled
* binary is never a FIPS build), the three name lists bake as fresh
@@ -3468,6 +3597,34 @@ declare module "crypto" {
* per member like http2.constants. */
export function getFips(): 1 | 0;
export function getCiphers(): string[];
export type CipherMode =
| "cbc"
| "ccm"
| "cfb"
| "ctr"
| "ecb"
| "gcm"
| "ocb"
| "ofb"
| "stream"
| "wrap"
| "xts";
export interface CipherInfoOptions {
keyLength?: number | undefined;
ivLength?: number | undefined;
}
export interface CipherInfo {
name: string;
nid: number;
blockSize?: number | undefined;
ivLength?: number | undefined;
keyLength: number;
mode: CipherMode;
}
export function getCipherInfo(
nameOrNid: string | number,
options?: CipherInfoOptions,
): CipherInfo | undefined;
export function getHashes(): string[];
export function getCurves(): string[];
/* Node v24 crypto.constants, literal-typed (the Http2Constants
@@ -4743,9 +4900,9 @@ declare module "node:readline/promises" {
* inputs with Node's default options; libz links only into zlib-using
* binaries. Callback forms use the executable worker pool. Options stay
* declared so reached uses receive a named SC2020 refusal. The Deflate,
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip streams are
* runtime Transform streams; Brotli and Zstd remain outside the lowered
* surface. */
* Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, Unzip, BrotliCompress
* and BrotliDecompress streams are runtime Transform streams; Zstd remains
* outside the lowered surface. */
declare module "zlib" {
import { Transform } from "stream";
export interface ZlibOptions {
@@ -4802,50 +4959,66 @@ declare module "zlib" {
): void;
export function unzipSync(data: string | Uint8Array, options?: ZlibOptions): Buffer;
export function crc32(data: string | Uint8Array, value?: number): number;
export function brotliCompressSync(data: string | Uint8Array): Buffer;
export function brotliDecompressSync(data: Uint8Array): Buffer;
/* Brotli needs a codec the static runtime does not vendor: these are
* declared so their uses receive the named refusal. */
/* Brotli over the vendored brotli 1.2.0 (scr_brotli.c): `params` keys
* are the numeric BROTLI_PARAM_* and BROTLI_DECODER_PARAM_* constants. */
export interface BrotliOptions {
[option: string]: unknown;
}
export function brotliCompress(data: string | Uint8Array, callback: ZlibCallback): void;
export function brotliDecompress(data: Uint8Array, callback: ZlibCallback): void;
export function createBrotliCompress(options?: ZlibOptions): Transform;
export function createBrotliDecompress(options?: ZlibOptions): Transform;
export class BrotliCompress extends Transform {
constructor(options?: ZlibOptions);
}
export class BrotliDecompress extends Transform {
constructor(options?: ZlibOptions);
}
export function brotliCompress(
data: string | Uint8Array,
options: BrotliOptions,
callback: ZlibCallback,
): void;
export function brotliCompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
export function brotliDecompress(data: string | Uint8Array, callback: ZlibCallback): void;
export function brotliDecompress(
data: string | Uint8Array,
options: BrotliOptions,
callback: ZlibCallback,
): void;
export function brotliDecompressSync(data: string | Uint8Array, options?: BrotliOptions): Buffer;
/* The stream classes: runtime Transform streams over the zlib engine
* (scr_zlib_stream.c). createGzip(options) and new Gzip(options) build
* the same stream; options are validated at runtime with Node's rules. */
* the same stream; options are validated at runtime with Node's rules.
* Brotli streams have no params(), as in Node. */
class ZlibBase extends Transform {
readonly bytesWritten: number;
flush(kind?: number, callback?: () => void): void;
flush(callback: () => void): void;
params(level: number, strategy: number, callback?: () => void): void;
close(callback?: () => void): void;
reset(): void;
}
export class Deflate extends ZlibBase {
class Zlib extends ZlibBase {
params(level: number, strategy: number, callback?: () => void): void;
}
export class BrotliCompress extends ZlibBase {
constructor(options?: BrotliOptions);
}
export class BrotliDecompress extends ZlibBase {
constructor(options?: BrotliOptions);
}
export function createBrotliCompress(options?: BrotliOptions): BrotliCompress;
export function createBrotliDecompress(options?: BrotliOptions): BrotliDecompress;
export class Deflate extends Zlib {
constructor(options?: ZlibOptions);
}
export class Inflate extends ZlibBase {
export class Inflate extends Zlib {
constructor(options?: ZlibOptions);
}
export class Gzip extends ZlibBase {
export class Gzip extends Zlib {
constructor(options?: ZlibOptions);
}
export class Gunzip extends ZlibBase {
export class Gunzip extends Zlib {
constructor(options?: ZlibOptions);
}
export class DeflateRaw extends ZlibBase {
export class DeflateRaw extends Zlib {
constructor(options?: ZlibOptions);
}
export class InflateRaw extends ZlibBase {
export class InflateRaw extends Zlib {
constructor(options?: ZlibOptions);
}
export class Unzip extends ZlibBase {
export class Unzip extends Zlib {
constructor(options?: ZlibOptions);
}
export function createDeflate(options?: ZlibOptions): Deflate;
@@ -5328,6 +5501,11 @@ declare module "net" {
/* The demux route: hand an accepted socket to another server. */
emit(event: "connection", socket: Socket): boolean;
on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
/* tls and https servers: a handshake that failed */
on(
event: "tlsClientError",
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
): void;
on(event: "close" | "listening", listener: () => void): void;
on(event: "drop", listener: (data?: DropArgument) => void): void;
on(event: "timeout", listener: (socket: Socket) => void): void;
@@ -5347,6 +5525,11 @@ declare module "net" {
): void;
/* addListener IS on (Node aliases them) — the suite spells both. */
addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
/* tls and https servers: a handshake that failed */
addListener(
event: "tlsClientError",
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
): void;
addListener(event: "close" | "listening", listener: () => void): void;
addListener(event: "drop", listener: (data?: DropArgument) => void): void;
addListener(event: "timeout", listener: (socket: Socket) => void): void;
@@ -5360,6 +5543,11 @@ declare module "net" {
listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void,
): void;
once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
/* tls and https servers: a handshake that failed */
once(
event: "tlsClientError",
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
): void;
once(event: "close" | "listening", listener: () => void): void;
once(event: "drop", listener: (data?: DropArgument) => void): void;
once(event: "timeout", listener: (socket: Socket) => void): void;
@@ -5561,6 +5749,7 @@ declare module "http" {
readonly trailersDistinct: { [name: string]: string[] | undefined };
readonly rawTrailers: string[];
resume(): void;
pause(): void;
destroy(): void;
setTimeout(msecs: number, callback?: () => void): this;
/* setEncoding('utf8'): 'data' delivers strings (other real encodings
@@ -5812,8 +6001,15 @@ declare module "node:http" {
* are PEM strings or Buffers. Handshake failures tear the socket down
* without emitting an event, Node's default for 'tlsClientError' with no listener. */
declare module "tls" {
import { Server, Socket } from "net";
export { Server };
import { Server as NetServer, Socket } from "net";
/** tls.Server: a net server whose connections handshake first. */
export interface Server extends NetServer {
/* serve `context` to clients whose SNI name matches `hostname` ('*'
* matches within one label); the newest match wins */
addContext(hostname: string, context: TlsOptions | SecureContext): void;
/* new connections serve a context made from `options` */
setSecureContext(options: TlsOptions): void;
}
export interface TlsOptions {
/* Optional in the DECLARATION only (real Node programs build these
* literals with every TLS knob): the lowering still requires both —
@@ -5847,7 +6043,47 @@ declare module "tls" {
getCipher(): CipherNameAndProtocol;
/* whether the handshake resumed a cached session */
isSessionReused(): boolean;
/* the peer's certificate: {} before the handshake or without one,
* null once the socket is destroyed; detailed adds issuerCertificate */
getPeerCertificate(detailed: true): DetailedPeerCertificate;
getPeerCertificate(detailed?: false): PeerCertificate;
getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate;
/* the certificate this socket serves, the same way */
getCertificate(): PeerCertificate | object | null;
/* a new X509Certificate on each call, or undefined without one */
getPeerX509Certificate(): import("crypto").X509Certificate | undefined;
getX509Certificate(): import("crypto").X509Certificate | undefined;
}
/* A certificate as getPeerCertificate() and X509Certificate's
* toLegacyObject() answer it: subject and issuer map attribute short
* names to values, infoAccess its "<method> - <type>" keys to values. */
export interface PeerCertificate {
subject: { [attribute: string]: string | string[] };
issuer: { [attribute: string]: string | string[] };
subjectaltname?: string;
infoAccess?: { [key: string]: string[] };
ca: boolean;
modulus?: string;
exponent?: string;
pubkey?: Buffer;
bits?: number;
valid_from: string;
valid_to: string;
fingerprint: string;
fingerprint256: string;
fingerprint512: string;
ext_key_usage?: string[];
serialNumber: string;
raw: Buffer;
asn1Curve?: string;
nistCurve?: string;
}
/* getPeerCertificate(true): each certificate links to its issuer; a
* self-issued root is its own issuerCertificate */
export interface DetailedPeerCertificate extends PeerCertificate {
issuerCertificate: DetailedPeerCertificate;
}
/** new tls.TLSSocket(socket, options): the server-mode wrap of an open
* socket (a server connection or an http CONNECT socket). The options
* record reads at runtime; isServer, cert, key, secureContext,
@@ -5902,6 +6138,8 @@ declare module "tls" {
/** An opaque parsed cert/key pair — what an SNI callback answers with
* (the value is a runtime handle; the members are runtime-internal). */
export interface SecureContext {}
/* @types/node's name for createSecureContext's options */
export type SecureContextOptions = TlsOptions;
export function createSecureContext(options: TlsOptions): SecureContext;
/** The CA-store introspection surface (scr_tls_ca.c): per-type cached
* PEM string arrays. The host bundle stands in for Node's compiled-in
@@ -5986,6 +6224,7 @@ declare module "http2" {
export interface Http2Stream {
on(event: "error" | "close", listener: (err: Error) => void): void;
once(event: "error" | "close", listener: (err: Error) => void): void;
addListener(event: "error" | "close", listener: (err: Error) => void): void;
destroy(): void;
}
export interface Http2ServerRequest {
@@ -6086,11 +6325,6 @@ declare module "http2" {
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
): void;
on(event: "session", listener: (session: ServerHttp2Session) => void): void;
once(
event: "stream",
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
): void;
once(event: "session", listener: (session: ServerHttp2Session) => void): void;
/* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the
* allowHTTP1 lowering never sees): the second argument is the raw
* socket on HTTP/1.1 connections; narrow with instanceof. */
@@ -6108,6 +6342,42 @@ declare module "http2" {
): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "close", listener: () => void): void;
once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void;
once(
event: "stream",
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
): void;
once(event: "session", listener: (session: ServerHttp2Session) => void): void;
once(
event: "connect",
listener: (
req: Http2ServerRequest | import("http").IncomingMessage,
resOrSocket: Http2ServerResponse | Socket,
) => void,
): void;
addListener(event: "connection", listener: (socket: Socket) => void): void;
addListener(
event: "request",
listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void,
): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(event: "close", listener: () => void): void;
addListener(
event: "sessionError",
listener: (err: Error, session: ServerHttp2Session) => void,
): void;
addListener(
event: "stream",
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
): void;
addListener(event: "session", listener: (session: ServerHttp2Session) => void): void;
addListener(
event: "connect",
listener: (
req: Http2ServerRequest | import("http").IncomingMessage,
resOrSocket: Http2ServerResponse | Socket,
) => void,
): void;
}
export interface SecureServerOptions {
allowHTTP1?: boolean;
@@ -6449,6 +6719,28 @@ declare module "http2" {
listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void,
): void;
once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void;
once(event: "frameError", listener: (type: number, code: number, id: number) => void): void;
once(event: "ping", listener: (payload: Uint8Array) => void): void;
addListener(event: "close" | "timeout", listener: () => void): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(
event: "connect",
listener: (session: Http2Session, socket: import("net").Socket) => void,
): void;
addListener(
event: "stream",
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
): void;
addListener(
event: "goaway",
listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void,
): void;
addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void;
addListener(
event: "frameError",
listener: (type: number, code: number, id: number) => void,
): void;
addListener(event: "ping", listener: (payload: Uint8Array) => void): void;
}
export interface ClientHttp2Session extends Http2Session {
@@ -6526,7 +6818,30 @@ declare module "http2" {
listener: () => void,
): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "frameError", listener: (type: number, code: number, id: number) => void): void;
once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void;
addListener(event: "data", listener: (chunk: any) => void): void;
addListener(
event:
| "end"
| "close"
| "aborted"
| "ready"
| "timeout"
| "drain"
| "finish"
| "wantTrailers",
listener: () => void,
): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(
event: "frameError",
listener: (type: number, code: number, id: number) => void,
): void;
addListener(
event: "trailers",
listener: (trailers: IncomingHttpHeaders, flags: number) => void,
): void;
respond(
headers?: OutgoingHttpHeaders,
options?: { endStream?: boolean; waitForTrailers?: boolean },
@@ -6576,6 +6891,7 @@ declare module "http2" {
on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void;
once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void;
once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void;
once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void;
once(event: "data", listener: (chunk: any) => void): void;
once(
event:
@@ -6590,6 +6906,42 @@ declare module "http2" {
listener: () => void,
): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "frameError", listener: (type: number, code: number, id: number) => void): void;
once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void;
addListener(
event: "response",
listener: (headers: IncomingHttpHeaders, flags: number) => void,
): void;
addListener(
event: "push",
listener: (headers: IncomingHttpHeaders, flags: number) => void,
): void;
addListener(
event: "headers",
listener: (headers: IncomingHttpHeaders, flags: number) => void,
): void;
addListener(event: "data", listener: (chunk: any) => void): void;
addListener(
event:
| "end"
| "close"
| "aborted"
| "ready"
| "timeout"
| "drain"
| "finish"
| "wantTrailers",
listener: () => void,
): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(
event: "frameError",
listener: (type: number, code: number, id: number) => void,
): void;
addListener(
event: "trailers",
listener: (trailers: IncomingHttpHeaders, flags: number) => void,
): void;
}
export interface Http2Server {
@@ -6624,6 +6976,24 @@ declare module "http2" {
): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "close" | "listening" | "timeout", listener: () => void): void;
once(event: "connection", listener: (socket: import("net").Socket) => void): void;
once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void;
addListener(
event: "stream",
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
): void;
addListener(event: "session", listener: (session: ServerHttp2Session) => void): void;
addListener(
event: "request",
listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void,
): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(event: "close" | "listening" | "timeout", listener: () => void): void;
addListener(event: "connection", listener: (socket: import("net").Socket) => void): void;
addListener(
event: "sessionError",
listener: (err: Error, session: ServerHttp2Session) => void,
): void;
}
export interface ServerOptions {
@@ -7022,9 +7392,18 @@ declare module "node:tty" {
declare module "async_hooks" {
/* AsyncLocalStorage: fiber-carried context snapshots — run/getStore/
* exit/enterWith/disable lower onto the runtime's active-slot machinery
* (als.* libCalls); values cross as dyn values. */
* (als.* libCalls); values cross as dyn values. bind and snapshot bind
* functions to the active context; the options carry the store's
* default value and name. */
export interface AsyncLocalStorageOptions {
defaultValue?: any;
name?: string | undefined;
}
export class AsyncLocalStorage<T = any> {
constructor();
constructor(options?: AsyncLocalStorageOptions);
static bind<Func extends (...args: any[]) => any>(fn: Func): Func;
static snapshot(): <R, TArgs extends any[]>(fn: (...args: TArgs) => R, ...args: TArgs) => R;
readonly name: string;
run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R;
exit<R>(callback: (...args: any[]) => R, ...args: any[]): R;
getStore(): T | undefined;
+21
View File
@@ -70,3 +70,24 @@ interface PromiseConstructor {
reject: (reason: Error) => void;
};
}
/* Carriers of the types the lowerer gives untyped JavaScript collections
* whose uses prove them (js-collection-types.ts). Nothing reads them at run
* time. */
declare namespace __scriptcInferredCollections {
const map_string_string: Map<string, string>;
const map_string_number: Map<string, number>;
const map_string_boolean: Map<string, boolean>;
const map_number_string: Map<number, string>;
const map_number_number: Map<number, number>;
const map_number_boolean: Map<number, boolean>;
const map_boolean_string: Map<boolean, string>;
const map_boolean_number: Map<boolean, number>;
const map_boolean_boolean: Map<boolean, boolean>;
const set_string: Set<string>;
const set_number: Set<number>;
const set_boolean: Set<boolean>;
const array_string: string[];
const array_number: number[];
const array_boolean: boolean[];
}
Binary file not shown.
+13
View File
@@ -135,3 +135,16 @@ void scr_cyc_set_growth_cap(long cap);
void scriptc_native_bound_cycle_growth(void) {
scr_cyc_set_growth_cap(2);
}
/* The runtime's nursery trigger (scr_rc.h). */
void scr_cyc_set_nursery_threshold(long threshold);
/* Runs nursery passes every 1024 new candidates instead of every 256. Much
* of a build's cyclic garbage outlives 256 releases: in the shorter window
* it survives a nursery pass, is promoted, and only full passes over the
* whole live heap find it. A constructor sets it before the compiler's
* modules initialize, so every pass of a build uses it.
* SCR_CYCLE_THRESHOLD still overrides it. */
__attribute__((constructor)) static void scriptc_native_widen_cycle_nursery(void) {
scr_cyc_set_nursery_threshold(1024);
}
+33
View File
@@ -0,0 +1,33 @@
/* Reads cases from standard input, each a little-endian u32 length, a u32
* start and the bytes, and prints scriptc_ts7_json_value_end for each. */
#include "ts7-process.h"
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#endif
static int read_u32(uint32_t *value) {
unsigned char bytes[4];
if (fread(bytes, 1, 4, stdin) != 4) return 0;
*value = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 | (uint32_t)bytes[2] << 16 |
(uint32_t)bytes[3] << 24;
return 1;
}
int main(void) {
#ifdef _WIN32
/* The cases are bytes: text mode would rewrite line endings. */
_setmode(_fileno(stdin), _O_BINARY);
#endif
uint32_t length, start;
while (read_u32(&length)) {
if (!read_u32(&start)) return 1;
uint8_t *bytes = malloc(length == 0 ? 1 : length);
if (bytes == NULL || fread(bytes, 1, length, stdin) != length) return 1;
printf("%d\n", (int)scriptc_ts7_json_value_end(bytes, length, start));
free(bytes);
}
return 0;
}
@@ -12,7 +12,49 @@
#include <io.h>
#include <windows.h>
#else
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* `--listen PATH --idle-exit SECONDS`: a stand-in for a shared server. In
* mode "listen-exit" it exits as an executable without --listen does. In
* mode "listen" it answers each client, in order, with its client number,
* its listen arguments and TSC_TS_LISTEN_MEMORY, then echoes until the
* client closes, and exits after SECONDS (at most 5) without a client. */
static int listen_main(const char *mode, char **argv) {
if (strcmp(mode, "listen-exit") == 0) return 2;
if (strcmp(mode, "listen") != 0) return 73;
struct sockaddr_un address;
memset(&address, 0, sizeof(address));
address.sun_family = AF_UNIX;
if (strlen(argv[0]) >= sizeof(address.sun_path)) return 77;
strcpy(address.sun_path, argv[0]);
int listener = socket(AF_UNIX, SOCK_STREAM, 0);
if (listener < 0 || bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 ||
listen(listener, 4) != 0)
return 78;
const char *memory = getenv("TSC_TS_LISTEN_MEMORY");
int idle = atoi(argv[2]);
if (idle > 5) idle = 5;
for (int client = 1;; client++) {
struct pollfd entry = {listener, POLLIN, 0};
if (poll(&entry, 1, idle * 1000) <= 0) break;
int fd = accept(listener, NULL, NULL);
if (fd < 0) return 79;
char line[512];
int size = snprintf(line, sizeof(line), "client %d\n%s %s\n%s\n", client, argv[0], argv[2],
memory == NULL ? "unset" : memory);
if (size <= 0 || write(fd, line, (size_t)size) != size) return 80;
uint8_t bytes[17];
ssize_t count;
while ((count = read(fd, bytes, sizeof(bytes))) > 0)
if (write(fd, bytes, (size_t)count) != count) break;
close(fd);
}
unlink(argv[0]);
return 0;
}
#endif
static int child_main(int argc, char **argv) {
@@ -20,15 +62,25 @@ static int child_main(int argc, char **argv) {
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
#endif
if (argc < 5 || argc > 6 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == 6 && strcmp(argv[5], "--timing") != 0)) return 71;
int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0;
int fixed = listening ? 9 : 5;
int at = fixed, timing = 0, libraries = 0;
if (at < argc && strcmp(argv[at], "--timing") == 0) { timing = 1; at++; }
if (at < argc && strcmp(argv[at], "--default-libraries-from-disk") == 0) { libraries = 1; at++; }
if (argc < fixed || at != argc || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
strncmp(argv[4], "--callbacks=", 12) != 0)
return 71;
const char *mode = argv[3];
#ifndef _WIN32
if (listening) return listen_main(mode, argv + 6);
#endif
if (strncmp(mode, "argv:", 5) == 0) {
fwrite(mode + 5, 1, strlen(mode + 5), stdout);
fputc('\n', stdout);
fputs(argv[4], stdout);
fputc('\n', stdout);
fputs(argc == 6 ? "timing\n" : "plain\n", stdout);
fputs(timing ? "timing\n" : "plain\n", stdout);
if (libraries) fputs("libraries from disk\n", stdout);
return ferror(stdout) ? 72 : 0;
}
if (strcmp(mode, "exit") == 0) return 0;
+251 -7
View File
@@ -1,5 +1,8 @@
#ifndef _WIN32
#define _POSIX_C_SOURCE 200809L
/* flock(2), for the checker server's lock file, and syscall(2). */
#define _DEFAULT_SOURCE
#define _DARWIN_C_SOURCE
#endif
#include "ts7-process.h"
#include <errno.h>
@@ -16,7 +19,13 @@
#include <fcntl.h>
#include <signal.h>
#include <spawn.h>
#include <sys/file.h>
#include <sys/socket.h>
#include <sys/stat.h>
#ifdef __linux__
#include <sys/syscall.h>
#endif
#include <sys/un.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
@@ -38,6 +47,9 @@ typedef struct Ts7Process {
HANDLE output;
#else
int socket;
/* The checker server's lock file, held while connected to the server
* (scriptc_ts7_attach); -1 for a server this process started. */
int lock;
#endif
} Ts7Process;
@@ -149,7 +161,7 @@ static wchar_t *command_line(wchar_t **arguments, size_t count) {
}
static int start_process(Ts7Process *p, char **args, size_t argc) {
wchar_t *wide[6] = {0};
wchar_t *wide[7] = {0};
wchar_t *line = NULL;
HANDLE child_input = NULL, child_output = NULL, child_error = NULL;
LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL;
@@ -283,7 +295,7 @@ static int start_process(Ts7Process *p, char **args, size_t argc) {
if (error == 0) error = posix_spawn(&pid, args[0], &actions, NULL, args, environment);
posix_spawn_file_actions_destroy(&actions);
if (error == 0) {
p->socket = sockets[0]; p->pid = (uint32_t)pid;
p->socket = sockets[0]; p->pid = (uint32_t)pid; p->lock = -1;
}
done:
close(sockets[1]);
@@ -293,15 +305,21 @@ done:
static void stop_process(Ts7Process *p) {
close(p->socket);
// A shared checker server keeps running; closing the connection ends this
// build's session, and closing the lock file lets another build connect.
if (p->lock >= 0) { close(p->lock); return; }
int status;
pid_t pid = (pid_t)p->pid;
for (int attempt = 0; attempt < 5; attempt++) {
// A server exits within a fraction of a millisecond of reading EOF, so the
// checks start 0.1 ms apart and double: 51.1 ms in all before the kill.
for (int attempt = 0; attempt < 10; attempt++) {
pid_t result;
do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR);
// ECHILD also means the PID no longer belongs to us. Never signal a
// potentially recycled PID after somebody else has reaped the child.
if (result == pid || (result < 0 && errno == ECHILD)) return;
struct timespec delay = {0, 10000000};
if (attempt == 9) break;
struct timespec delay = {0, 100000L << attempt};
while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {}
}
(void)kill(pid, SIGKILL);
@@ -312,7 +330,7 @@ static void stop_process(Ts7Process *p) {
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
const uint8_t *cwd, size_t cwd_size,
const uint8_t *callbacks, size_t callbacks_size,
uint8_t timing) {
uint8_t timing, uint8_t libraries_from_disk) {
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
char *exe = copy_argument(executable, executable_size, "");
if (exe == NULL) return 0;
@@ -328,8 +346,12 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
}
exit_registered = 1;
}
char *args[] = {exe, "--api", "--cwd", directory, callback_flag, timing ? "--timing" : NULL, NULL};
int started = start_process(p, args, timing ? 6 : 5);
char *args[8] = {exe, "--api", "--cwd", directory, callback_flag};
size_t argc = 5;
if (timing) args[argc++] = "--timing";
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
args[argc] = NULL;
int started = start_process(p, args, argc);
free(exe); free(directory); free(callback_flag);
if (!started) { free(p); return 0; }
p->id = next_id++;
@@ -338,6 +360,184 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
return p->id;
}
#ifdef _WIN32
uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
const uint8_t *executable, size_t executable_size,
const uint8_t *cwd, size_t cwd_size,
const uint8_t *callbacks, size_t callbacks_size,
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
uint32_t memory) {
(void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size;
(void)callbacks; (void)callbacks_size; (void)timing; (void)libraries_from_disk; (void)idle_exit; (void)memory;
error_text("a shared checker server needs Unix sockets");
return 0;
}
#else
/* This user's checker server directory: $XDG_RUNTIME_DIR/scriptc, or
* /tmp/scriptc-<uid>. It must be a directory this user owns with no access
* for others; one that exists otherwise is not used. */
static int server_directory(char *path, size_t size) {
const char *runtime = getenv("XDG_RUNTIME_DIR");
int count = runtime != NULL && runtime[0] == '/'
? snprintf(path, size, "%s/scriptc", runtime)
: snprintf(path, size, "/tmp/scriptc-%lu", (unsigned long)geteuid());
if (count < 0 || (size_t)count >= size) { error_text("checker server directory path is too long"); return 0; }
if (mkdir(path, 0700) != 0 && errno != EEXIST) { system_error(errno); return 0; }
struct stat info;
if (lstat(path, &info) != 0) { system_error(errno); return 0; }
if (!S_ISDIR(info.st_mode) || info.st_uid != geteuid() || (info.st_mode & 077) != 0) {
error_text("checker server directory is not private to this user");
return 0;
}
return 1;
}
/* A connection to the server listening at `path`, or -1. */
static int connect_server(const char *path) {
struct sockaddr_un address;
memset(&address, 0, sizeof(address));
address.sun_family = AF_UNIX;
memcpy(address.sun_path, path, strlen(path) + 1);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { close(fd); return -1; }
#ifdef SO_NOSIGPIPE
int no_sigpipe = 1;
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe)) != 0) { close(fd); return -1; }
#endif
int status;
do { status = connect(fd, (const struct sockaddr *)&address, sizeof(address)); } while (status != 0 && errno == EINTR);
if (status != 0) { close(fd); return -1; }
return fd;
}
/* Starts the server in its own process group, so that a terminal's signals
* to the build do not reach it, with its standard streams on /dev/null and
* no other descriptor of this process, so that it holds no pipe a caller of
* the build waits on. `limit` (TSC_TS_LISTEN_MEMORY=...) is added to the
* environment unless it sets TSC_TS_LISTEN_MEMORY. */
static pid_t start_server(char **args, char *limit) {
#ifdef __APPLE__
char **inherited = *_NSGetEnviron();
#else
char **inherited = environ;
#endif
size_t count = 0;
int has_limit = 0;
for (; inherited[count] != NULL; count++)
if (strncmp(inherited[count], "TSC_TS_LISTEN_MEMORY=", 21) == 0) has_limit = 1;
char **environment = malloc((count + 2) * sizeof(char *));
if (environment == NULL) { error_text("out of memory"); return -1; }
memcpy(environment, inherited, count * sizeof(char *));
environment[count] = has_limit ? NULL : limit;
environment[count + 1] = NULL;
long descriptors = sysconf(_SC_OPEN_MAX);
if (descriptors < 0 || descriptors > 65536) descriptors = 65536;
sigset_t signals;
sigemptyset(&signals);
pid_t pid = fork();
if (pid == 0) {
// Only async-signal-safe calls until exec.
int null = open("/dev/null", O_RDWR);
if (setpgid(0, 0) != 0 || null < 0 || dup2(null, STDIN_FILENO) < 0 || dup2(null, STDOUT_FILENO) < 0 ||
dup2(null, STDERR_FILENO) < 0)
_exit(127);
#if defined(__linux__) && defined(SYS_close_range)
if (syscall(SYS_close_range, 3U, ~0U, 0U) != 0)
#endif
for (long fd = 3; fd < descriptors; fd++) close((int)fd);
sigprocmask(SIG_SETMASK, &signals, NULL);
execve(args[0], args, environment);
_exit(127);
}
int error = errno;
free(environment);
if (pid < 0) { system_error(error); return -1; }
return pid;
}
uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
const uint8_t *executable, size_t executable_size,
const uint8_t *cwd, size_t cwd_size,
const uint8_t *callbacks, size_t callbacks_size,
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
uint32_t memory) {
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
char *server = copy_argument(name, name_size, "");
if (server == NULL) return 0;
char directory[256], socket_path[sizeof(((struct sockaddr_un *)0)->sun_path)], lock_path[512];
int named = strchr(server, '/') == NULL;
if (!named) error_text("checker server names cannot contain '/'");
if (named) named = server_directory(directory, sizeof(directory));
if (named) {
int count = snprintf(socket_path, sizeof(socket_path), "%s/%s.sock", directory, server);
named = count > 0 && (size_t)count < sizeof(socket_path);
if (!named) error_text("checker server socket path is too long");
else (void)snprintf(lock_path, sizeof(lock_path), "%s.lock", socket_path);
}
free(server);
if (!named) return 0;
// One build at a time uses the server; a build that finds it in use
// starts its own server instead of waiting.
int lock = open(lock_path, O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600);
if (lock < 0) { system_error(errno); return 0; }
if (flock(lock, LOCK_EX | LOCK_NB) != 0) {
if (errno == EWOULDBLOCK) error_text("the checker server is in use");
else system_error(errno);
close(lock);
return 0;
}
int fd = connect_server(socket_path);
if (fd < 0) {
char *exe = copy_argument(executable, executable_size, "");
char *dir = exe == NULL ? NULL : copy_argument(cwd, cwd_size, "");
char *callback_flag = dir == NULL ? NULL : copy_argument(callbacks, callbacks_size, "--callbacks=");
char idle[16], limit[48];
(void)snprintf(idle, sizeof(idle), "%lu", (unsigned long)idle_exit);
(void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory);
pid_t pid = -1;
if (callback_flag != NULL) {
char *args[12] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle};
size_t argc = 9;
if (timing) args[argc++] = "--timing";
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
args[argc] = NULL;
pid = start_server(args, limit);
}
free(exe); free(dir); free(callback_flag);
// The server listens as it starts. One that exits first, such as a
// tsc-ts without --listen, or one that is not listening within 2 s,
// leaves this build to start its own server.
for (int attempt = 0; pid > 0; attempt++) {
fd = connect_server(socket_path);
if (fd >= 0) break;
int status;
pid_t result;
do { result = waitpid(pid, &status, WNOHANG); } while (result < 0 && errno == EINTR);
if (result != 0) { error_text("the checker server exited as it started"); break; }
if (attempt == 1000) { error_text("the checker server did not start listening"); break; }
struct timespec delay = {0, 2000000};
while (nanosleep(&delay, &delay) != 0 && errno == EINTR) {}
}
}
if (fd < 0) { close(lock); return 0; }
Ts7Process *p = calloc(1, sizeof(*p));
if (p == NULL) { error_text("out of memory"); close(fd); close(lock); return 0; }
if (!exit_registered) {
if (atexit(scriptc_ts7_close_all) != 0) {
error_text("could not register server cleanup"); free(p); close(fd); close(lock); return 0;
}
exit_registered = 1;
}
p->socket = fd;
p->lock = lock;
p->id = next_id++;
p->next = processes;
processes = p;
return p->id;
}
#endif
uint32_t scriptc_ts7_pid(uint32_t handle) {
Ts7Process *p = find_process(handle);
return p != NULL ? p->pid : 0;
@@ -405,3 +605,47 @@ void scriptc_ts7_close(uint32_t handle) {
void scriptc_ts7_close_all(void) {
while (processes != NULL) scriptc_ts7_close(processes->id);
}
static int json_space(uint8_t byte) {
return byte == ' ' || byte == '\n' || byte == '\r' || byte == '\t';
}
int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start) {
if (size > INT32_MAX) return -1;
size_t depth = 0;
size_t index = start;
while (index < size) {
uint8_t byte = bytes[index];
if (byte == '"') {
index++;
// The quote found stays the candidate end until an escape skips it, so
// every byte of the string is scanned once by each search.
const uint8_t *quote = index < size ? memchr(bytes + index, '"', size - index) : NULL;
for (;;) {
if (quote == NULL) return -1;
const uint8_t *escape = memchr(bytes + index, '\\', (size_t)(quote - (bytes + index)));
if (escape == NULL) {
index = (size_t)(quote - bytes);
break;
}
index = (size_t)(escape - bytes) + 2;
if (bytes + index > quote) quote = index < size ? memchr(bytes + index, '"', size - index) : NULL;
}
index++;
if (depth == 0) return (int32_t)index;
continue;
}
if (byte == '{' || byte == '[') {
depth++;
} else if (byte == '}' || byte == ']') {
if (depth == 0) return (int32_t)index;
depth--;
if (depth == 0) return (int32_t)(index + 1);
} else if (depth == 0 && (byte == ',' || json_space(byte))) {
return (int32_t)index;
}
index++;
}
if (depth != 0) return -1;
return (int32_t)index;
}
@@ -1,12 +1,14 @@
{
"ffi_format": 6,
"functions": [
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" },
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool", "bool"], "returns": "u32" },
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "bool", "u32", "u32"], "returns": "u32" },
{ "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" },
{ "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" },
{ "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" },
{ "name": "ts7NativeClose", "symbol": "scriptc_ts7_close", "params": ["u32"], "returns": "void" },
{ "name": "ts7NativeCloseAll", "symbol": "scriptc_ts7_close_all", "params": [], "returns": "void" },
{ "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" }
{ "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" },
{ "name": "ts7NativeJsonValueEnd", "symbol": "scriptc_ts7_json_value_end", "params": ["bytes", "u32"], "returns": "i32" }
]
}
+19 -1
View File
@@ -12,7 +12,21 @@
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
const uint8_t *cwd, size_t cwd_size,
const uint8_t *callbacks, size_t callbacks_size,
uint8_t timing);
uint8_t timing, uint8_t libraries_from_disk);
/* Connects to the shared checker server `name` (`--api --listen`) in this
* user's private server directory, starting it in its own process group
* with `--idle-exit idle_exit` and a limit of `memory` megabytes
* (TSC_TS_LISTEN_MEMORY, unless the environment sets it) when none answers. Returns 0 with the error
* text when another build is connected to it, it cannot be started or
* reached, or the platform has no Unix sockets; the caller then starts its
* own server with scriptc_ts7_open. Closing the handle ends the connection
* and leaves the server running. */
uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
const uint8_t *executable, size_t executable_size,
const uint8_t *cwd, size_t cwd_size,
const uint8_t *callbacks, size_t callbacks_size,
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
uint32_t memory);
int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size,
uint32_t offset, uint32_t length);
int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size,
@@ -21,5 +35,9 @@ uint32_t scriptc_ts7_pid(uint32_t handle);
void scriptc_ts7_close(uint32_t handle);
void scriptc_ts7_close_all(void);
int32_t scriptc_ts7_error(uint8_t *bytes, size_t size);
/* The index after the JSON value that starts at `start` in a checker reply,
* as jsonValueEnd in semantic-json.ts computes it, or -1 when the reply is
* malformed there. */
int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start);
#endif
+101 -9
View File
@@ -11,7 +11,9 @@
#include <windows.h>
#else
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
@@ -19,12 +21,14 @@
static const char *executable;
static const char *callbacks = "readFile,fileExists";
static uint32_t start(const char *mode, int timing) {
static uint32_t start_with(const char *mode, int timing, int libraries) {
return scriptc_ts7_open((const uint8_t *)executable, strlen(executable),
(const uint8_t *)mode, strlen(mode),
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing);
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing, (uint8_t)libraries);
}
static uint32_t start(const char *mode, int timing) { return start_with(mode, timing, 0); }
static void fail_message(void) {
uint8_t message[1024];
int32_t size = scriptc_ts7_error(message, sizeof(message));
@@ -81,11 +85,13 @@ static void argument_cases(void) {
"", "a b", "double\"quote", "end\\", "many\\\\\"quotes\\\\", "a;&$()`'b", "h\xc3\xa9llo \xf0\x9f\x8c\x8d",
};
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
for (int timing = 0; timing <= 1; timing++) {
for (int flags = 0; flags < 4; flags++) {
int timing = flags & 1, libraries = flags >> 1;
char mode[256], expected[512];
snprintf(mode, sizeof(mode), "argv:%s", values[i]);
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n", values[i], callbacks, timing ? "timing" : "plain");
uint32_t id = start(mode, timing);
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n%s", values[i], callbacks,
timing ? "timing" : "plain", libraries ? "libraries from disk\n" : "");
uint32_t id = start_with(mode, timing, libraries);
assert(id != 0);
uint8_t actual[512];
read_exact(id, actual, strlen(expected));
@@ -145,10 +151,10 @@ static void failure_cases(void) {
executable = "/scriptc/does-not-exist/tsgo";
for (int i = 0; i < 30; i++) { assert(start("echo", 0) == 0); fail_message(); }
executable = saved;
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0) == 0);
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0) == 0);
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0) == 0);
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0) == 0);
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0, 0) == 0);
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0, 0) == 0);
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0, 0) == 0);
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0, 0) == 0);
for (int i = 0; i < 30; i++) {
uint32_t id = start("exit", 0);
assert(id != 0);
@@ -171,6 +177,89 @@ static void failure_cases(void) {
scriptc_ts7_close(UINT32_MAX);
}
#ifndef _WIN32
static uint32_t attach(const char *name, const char *mode, uint32_t memory) {
return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable),
(const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks),
0, 0, 1, memory);
}
static void expect_line(uint32_t id, const char *expected) {
char line[512];
size_t at = 0;
do {
assert(at + 1 < sizeof(line));
read_exact(id, (uint8_t *)line + at, 1);
} while (line[at++] != '\n');
line[at] = '\0';
assert(strcmp(line, expected) == 0);
}
static void attach_cases(void) {
char runtime[] = "/tmp/scriptc-ts7-attach-XXXXXX";
assert(mkdtemp(runtime) != NULL);
assert(setenv("XDG_RUNTIME_DIR", runtime, 1) == 0);
assert(unsetenv("TSC_TS_LISTEN_MEMORY") == 0);
char directory[256], socket_path[512], expected[600];
uint8_t bytes[4];
snprintf(directory, sizeof(directory), "%s/scriptc", runtime);
snprintf(socket_path, sizeof(socket_path), "%s/server.sock", directory);
// The first build starts the server with its idle time and memory limit.
// The server holds none of the build's descriptors, such as an output
// pipe its caller reads to the end.
int output[2];
assert(pipe(output) == 0);
uint32_t first = attach("server", "listen", 123);
assert(first != 0 && scriptc_ts7_pid(first) == 0);
close(output[1]);
struct pollfd ended = {output[0], POLLIN, 0};
assert(poll(&ended, 1, 5000) == 1);
assert(read(output[0], bytes, 1) == 0);
close(output[0]);
expect_line(first, "client 1\n");
snprintf(expected, sizeof(expected), "%s 1\n", socket_path);
expect_line(first, expected);
expect_line(first, "123\n");
write_exact(first, (const uint8_t *)"ping", 4, 0, 4);
read_exact(first, bytes, 4);
assert(memcmp(bytes, "ping", 4) == 0);
// A build that finds the server in use gets no handle.
for (int i = 0; i < 3; i++) { assert(attach("server", "listen", 123) == 0); fail_message(); }
scriptc_ts7_close(first);
scriptc_ts7_close(first);
// A later build connects to the same server.
uint32_t second = attach("server", "listen", 123);
assert(second != 0);
expect_line(second, "client 2\n");
scriptc_ts7_close(second);
// An executable without --listen exits as it starts.
assert(attach("old", "listen-exit", 0) == 0);
fail_message();
// A server directory others can access is not used.
assert(chmod(directory, 0755) == 0);
assert(attach("server", "listen", 123) == 0);
fail_message();
assert(chmod(directory, 0700) == 0);
// A name that would leave the directory, or make the path too long.
assert(attach("../server", "listen", 123) == 0);
fail_message();
char long_name[200];
memset(long_name, 'x', sizeof(long_name) - 1);
long_name[sizeof(long_name) - 1] = '\0';
assert(attach(long_name, "listen", 123) == 0);
fail_message();
// The server exits after its idle second and removes its socket.
unlink(socket_path);
snprintf(expected, sizeof(expected), "%s.lock", socket_path);
unlink(expected);
snprintf(expected, sizeof(expected), "%s/old.sock.lock", directory);
unlink(expected);
rmdir(directory);
rmdir(runtime);
assert(unsetenv("XDG_RUNTIME_DIR") == 0);
}
#endif
static int process_main(int argc, char **argv) {
assert(argc >= 2);
executable = argv[1];
@@ -196,6 +285,9 @@ static int process_main(int argc, char **argv) {
argument_cases();
exchange_cases();
failure_cases();
#ifndef _WIN32
attach_cases();
#endif
assert(resource_count() == before);
#ifndef _WIN32
if (saved_input >= 0) { assert(dup2(saved_input, STDIN_FILENO) >= 0); close(saved_input); }
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/compiler",
"version": "0.4.2",
"version": "0.4.4",
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -40,7 +40,7 @@
},
"dependencies": {
"@scriptc/runtime": "workspace:*",
"tsc-ts": "0.0.8",
"tsc-ts": "0.0.9",
"typescript": "7.0.2",
"typescript5": "npm:typescript@5.9.3"
},
@@ -0,0 +1,645 @@
// scriptc's built-in implementation of `@hono/node-server` 2.x: the Node.js
// adapter that serves a fetch-style handler (Hono's `app.fetch`) over
// node:http. The published package swaps the global Request and Response
// for lightweight objects and reparents prototypes, which only pays off
// under V8; this port builds native Request objects from the incoming
// message, with a body streamed from it as the app reads it, and writes
// the handler's Response the way the package's listener does (header
// record, content length for bodies that complete within the first reads,
// streamed bodies, error responses), so a client sees the same status
// lines, headers and bodies. A body the app leaves unread is drained after
// the response, within the package's limits.
//
// HTTP/1.1 only: the http2/https server options and WebSocket upgrades are
// not declared. A Response that is still exactly its constructor arguments
// takes the package's cached-response path (its lightweight Response), read
// from the runtime through scriptcResponseLight; every other Response takes
// the general path.
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";
export type HttpBindings = {
incoming: IncomingMessage;
outgoing: ServerResponse;
};
export type ServerType = Server;
export type FetchCallback = (request: Request, env: HttpBindings) => Response | Promise<Response>;
export type CustomErrorHandler = (err: unknown) => void | Response | Promise<void | Response>;
export type Options = {
fetch: FetchCallback;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
port?: number;
hostname?: string;
};
export interface RequestListenerOptions {
hostname?: string;
errorHandler?: CustomErrorHandler;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
}
export class RequestError extends Error {
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
this.name = "RequestError";
}
}
const DEFAULT_CONTENT_TYPE = "text/plain; charset=UTF-8";
const X_ALREADY_SENT = "x-hono-already-sent";
const reValidRequestUrl = /^\/[!#$&-;=?-[\]_a-z~]*$/;
const reDotSegment = /\/\.\.?(?:[/?#]|$)/;
const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/;
function buildUrl(scheme: string, host: string, incomingUrl: string): string {
const url = `${scheme}://${host}${incomingUrl}`;
if (!reValidHost.test(host)) {
const urlObj = new URL(url);
if (
urlObj.hostname.length !== host.length &&
urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()
)
throw new RequestError("Invalid host header");
return urlObj.href;
}
if (incomingUrl.length === 0) return url + "/";
if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL");
if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl))
return new URL(url).href;
return url;
}
function normalizeIncomingMethod(method: string | undefined): string {
if (method === undefined || method.length === 0) return "GET";
switch (method) {
case "DELETE":
case "GET":
case "HEAD":
case "OPTIONS":
case "PATCH":
case "POST":
case "PUT":
case "QUERY":
return method;
}
const upper = method.toUpperCase();
switch (upper) {
case "DELETE":
case "GET":
case "HEAD":
case "OPTIONS":
case "POST":
case "PUT":
return upper;
default:
return method;
}
}
/** The request URL the package builds: an absolute request target as is,
* otherwise the Host header (or the configured hostname) over http. */
function requestUrl(incoming: IncomingMessage, defaultHostname: string | undefined): string {
const incomingUrl = incoming.url ?? "";
if (
incomingUrl[0] !== "/" &&
(incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))
) {
try {
return new URL(incomingUrl).href;
} catch (e) {
throw new RequestError("Invalid absolute URL", { cause: e });
}
}
const hostHeader = incoming.headers.host;
const host = hostHeader !== undefined && hostHeader !== "" ? hostHeader : (defaultHostname ?? "");
if (host === "") throw new RequestError("Missing host header");
try {
return buildUrl("http", host, incomingUrl);
} catch (e) {
if (e instanceof RequestError) throw e;
throw new RequestError("Invalid URL", { cause: e });
}
}
function requestHeaders(incoming: IncomingMessage): Headers {
const headers = new Headers();
const raw = incoming.rawHeaders;
for (let i = 0; i < raw.length; i += 2) {
const name = raw[i]!;
if (!name.startsWith(":")) headers.append(name, raw[i + 1]!);
}
return headers;
}
/** What the listener knows about the incoming body: whether it ended
* (the package's `readableEnded`) and whether a drain started. */
type IncomingState = { ended: boolean; draining: boolean };
/** The incoming message's readableHighWaterMark: Readable.toWeb queues
* this many bytes before it pauses the message. */
const BODY_HIGH_WATER_MARK = 64 * 1024;
/** The request body as the package streams it: Readable.toWeb over the
* incoming message, read one chunk per pull. Chunks queue until
* BODY_HIGH_WATER_MARK bytes wait unread, then the message pauses until a
* pull takes them, so the socket holds the rest. The stream pulls once on
* its own when it is constructed; the package builds its stream only when
* the app reads the body, so until the app's first read a drain discards
* the chunks instead of queueing them. */
function incomingBody(incoming: IncomingMessage, state: IncomingState): ReadableStream<Uint8Array> {
let pulls = 0;
let listening = false;
let ended = false;
let failure: Error | undefined = undefined;
const queue: Buffer[] = [];
let queued = 0;
let wake: (() => void) | undefined = undefined;
const woken = (): void => {
const resolve = wake;
wake = undefined;
if (resolve !== undefined) resolve();
};
const listen = (): void => {
listening = true;
incoming.on("data", (chunk: Buffer) => {
if (state.draining && pulls === 1) return;
queue.push(chunk);
queued += chunk.length;
if (queued >= BODY_HIGH_WATER_MARK) incoming.pause();
woken();
});
incoming.on("end", () => {
ended = true;
woken();
});
incoming.on("error", (err: Error) => {
failure = err;
woken();
});
};
return new ReadableStream<Uint8Array>({
async pull(controller) {
pulls++;
if (!listening) listen();
for (;;) {
const chunk = queue.shift();
if (chunk !== undefined) {
queued -= chunk.length;
if (queued < BODY_HIGH_WATER_MARK) incoming.resume();
controller.enqueue(chunk);
return;
}
if (failure !== undefined) {
controller.error(failure);
return;
}
if (ended) {
controller.close();
return;
}
await new Promise<void>((resolve) => {
wake = resolve;
});
}
},
});
}
function newRequest(
incoming: IncomingMessage,
defaultHostname: string | undefined,
state: IncomingState,
): Request {
const method = normalizeIncomingMethod(incoming.method);
const url = requestUrl(incoming, defaultHostname);
const headers = requestHeaders(incoming);
if (method === "GET" || method === "HEAD") return new Request(url, { method, headers });
return new Request(url, {
method,
headers,
body: incomingBody(incoming, state),
duplex: "half",
});
}
const DRAIN_TIMEOUT_MS = 500;
const MAX_DRAIN_BYTES = 64 * 1024 * 1024;
/** The package's drain of a body the app left unread: read and discard
* up to MAX_DRAIN_BYTES or DRAIN_TIMEOUT_MS, then close the connection
* once its writes are out. */
function drainIncoming(incoming: IncomingMessage, state: IncomingState): void {
const socket = incoming.socket;
if (socket.destroyed || state.draining) return;
state.draining = true;
let bytesRead = 0;
let done = false;
const cleanup = (): void => {
if (done) return;
done = true;
clearTimeout(timer);
};
const forceClose = (): void => {
if (done) return;
cleanup();
if (!socket.destroyed) socket.destroySoon();
};
const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS);
timer.unref();
incoming.on("data", (chunk: Buffer) => {
if (done) return;
bytesRead += chunk.length;
if (bytesRead > MAX_DRAIN_BYTES) forceClose();
});
incoming.on("end", cleanup);
incoming.on("error", cleanup);
incoming.resume();
}
/** The package's close handler cleanup: an unread body drains two timer
* turns after the response closes. */
function drainAfterClose(incoming: IncomingMessage, state: IncomingState): void {
if (state.ended) return;
setTimeout(() => {
if (!state.ended)
setTimeout(() => {
drainIncoming(incoming, state);
});
});
}
/** The package's outgoing header record: Headers order (lowercase,
* sorted), Set-Cookie values gathered into one array entry at the end,
* and the default content type for a response with a body. */
function buildOutgoingHttpHeaders(
headers: Headers,
hasBody: boolean,
): Record<string, string | string[]> {
const res: Record<string, string | string[]> = {};
if (headers.has("set-cookie")) {
const cookies: string[] = [];
headers.forEach((v, k) => {
if (k === "set-cookie") cookies.push(v);
else res[k] = v;
});
// Headers.forEach joins nothing but set-cookie, which getSetCookie
// lists value by value, as the package's iteration does.
if (cookies.length > 0) res["set-cookie"] = headers.getSetCookie();
} else {
headers.forEach((v, k) => {
res[k] = v;
});
}
if (hasBody && res["content-type"] === undefined) res["content-type"] = DEFAULT_CONTENT_TYPE;
return res;
}
/* Spelled out rather than ReadableStreamReadResult<Uint8Array>, whose
* declaration depends on the program's lib and @types/node settings: the
* shim compiles inside every program that imports the package. */
type ReadResult = { done: boolean; value?: Uint8Array };
function readWithoutBlocking(read: Promise<ReadResult>): Promise<ReadResult | undefined> {
return Promise.race([read, Promise.resolve().then(() => Promise.resolve(undefined))]);
}
function nextTick(): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(resolve);
});
}
/** Writes the rest of a body stream. As in the package, a response that
* closes before the stream ends (the client disconnected) cancels the
* stream, so its producer stops; the pending read then answers done. A
* response that fails closes too, and its stream is cancelled the same way
* (the package passes the error as the reason). The listener stays attached
* and does nothing once the stream has ended. */
async function pump(
reader: ReadableStreamDefaultReader<Uint8Array>,
outgoing: ServerResponse,
pending: Promise<ReadResult> | undefined,
): Promise<void> {
let finished = false;
outgoing.on("close", () => {
if (!finished) reader.cancel().catch(() => {});
});
try {
let read = pending ?? reader.read();
for (;;) {
const { done, value } = await read;
if (done) {
outgoing.end();
return;
}
outgoing.write(value!);
read = reader.read();
}
} finally {
finished = true;
}
}
function handleResponseError(e: unknown, outgoing: ServerResponse): void {
const err = e instanceof Error ? e : new Error("unknown error", { cause: e });
console.error(e);
if (!outgoing.headersSent) outgoing.writeHead(500, { "Content-Type": "text/plain" });
outgoing.end(`Error: ${err.message}`);
outgoing.destroy();
}
function emptyResponse(status: number): Response {
return new Response(null, { status });
}
/** The lightweight state of a constructed Response (the runtime's
* scr_fetch_response_light): [form, flags, byte length, ...record pairs]. */
declare function scriptcResponseLight(res: Response): string[];
const UNDICI_TEXT_TYPE = "text/plain;charset=UTF-8";
/** Content-Length as Node's implicit header: set the headers one by one and
* let end(body) compute the length, which Node then writes after its own
* Date/Connection headers, as the package's preset length does. */
function endWithImplicitLength(
outgoing: ServerResponse,
status: number,
header: Record<string, string | string[]>,
body: Uint8Array,
): void {
outgoing.statusCode = status;
for (const name of Object.keys(header)) outgoing.setHeader(name, header[name]!);
outgoing.end(body);
}
/** The package's cached-response path: a Response that is still exactly
* its constructor arguments (no body or metadata read since) is written
* from them. Form 1 has no init headers, form 2 Headers, form 3 a plain
* record kept verbatim. */
async function writeLightResponse(
res: Response,
outgoing: ServerResponse,
light: string[],
head: boolean,
): Promise<void> {
const form = light[0]!;
const flags = light[1]!;
const length = Number(light[2]!);
const status = res.status;
const sized = length >= 0;
if (form === "1") {
if (length === -1) {
outgoing.writeHead(status);
outgoing.end();
} else if (sized) {
const body = await res.bytes();
if (head) {
outgoing.writeHead(status, {
"Content-Type": DEFAULT_CONTENT_TYPE,
"Content-Length": String(length),
});
outgoing.end(body);
} else
endWithImplicitLength(outgoing, status, { "Content-Type": DEFAULT_CONTENT_TYPE }, body);
} else {
outgoing.writeHead(status, { "Content-Type": DEFAULT_CONTENT_TYPE });
outgoing.flushHeaders();
await pump(res.body!.getReader(), outgoing, undefined);
}
return;
}
let header: Record<string, string | string[]>;
let hasContentLength = false;
let plain = false;
let canAutoLength = true;
if (form === "2") {
const headers = new Headers(res.headers);
if (flags.includes("a") && headers.get("content-type") === UNDICI_TEXT_TYPE) {
if (flags.includes("d")) headers.set("content-type", DEFAULT_CONTENT_TYPE);
else headers.delete("content-type");
} else if (flags.includes("d") && !headers.has("content-type")) {
headers.set("content-type", DEFAULT_CONTENT_TYPE);
}
hasContentLength = headers.has("content-length");
header = buildOutgoingHttpHeaders(headers, length !== -1);
} else {
plain = true;
header = {};
for (let i = 3; i + 1 < light.length; i += 2) {
const name = light[i]!;
const value = light[i + 1]!;
const previous = header[name];
if (previous === undefined) header[name] = value;
else if (typeof previous === "string") header[name] = [previous, value];
else previous.push(value);
const lower = name.toLowerCase();
if (lower === "content-length") hasContentLength = true;
else if (lower === "transfer-encoding" || lower === "trailer") canAutoLength = false;
}
}
const body = sized ? await res.bytes() : undefined;
if (!hasContentLength && body !== undefined) {
if (plain && canAutoLength && !head && status !== 204 && status !== 304) {
endWithImplicitLength(outgoing, status, header, body);
return;
}
header["Content-Length"] = String(length);
}
outgoing.writeHead(status, header);
if (length === -1) outgoing.end();
else if (body !== undefined) outgoing.end(body);
else {
outgoing.flushHeaders();
await pump(res.body!.getReader(), outgoing, undefined);
}
}
async function writeResponse(
res: Response,
outgoing: ServerResponse,
head: boolean,
): Promise<void> {
const light = scriptcResponseLight(res);
if (light[0] !== "0") return writeLightResponse(res, outgoing, light, head);
const body = res.body;
const record = buildOutgoingHttpHeaders(res.headers, body !== null);
if (body !== null) {
const reader: ReadableStreamDefaultReader<Uint8Array> = body.getReader();
const values: Uint8Array[] = [];
let done = false;
let pending: Promise<ReadResult> | undefined = undefined;
if (record["transfer-encoding"] !== "chunked") {
let maxReadCount = 2;
for (let i = 0; i < maxReadCount; i++) {
pending ??= reader.read();
let chunk: ReadResult | undefined = undefined;
try {
chunk = await readWithoutBlocking(pending);
} catch (e) {
console.error(e);
done = true;
}
if (chunk === undefined) {
if (i === 1 && !done) {
await nextTick();
maxReadCount = 3;
continue;
}
break;
}
pending = undefined;
if (chunk.value !== undefined) values.push(chunk.value);
if (chunk.done) {
done = true;
break;
}
}
if (done && record["content-length"] === undefined) {
let length = 0;
for (const value of values) length += value.length;
record["content-length"] = String(length);
}
}
outgoing.writeHead(res.status, record);
for (const value of values) outgoing.write(value);
if (done) outgoing.end();
else {
if (values.length === 0) outgoing.flushHeaders();
await pump(reader, outgoing, pending);
}
} else if (record[X_ALREADY_SENT] === undefined) {
outgoing.writeHead(res.status, record);
outgoing.end();
}
}
async function respond(
pending: Response | Promise<Response>,
outgoing: ServerResponse,
errorHandler: CustomErrorHandler | undefined,
head: boolean,
): Promise<void> {
let res: Response;
if (pending instanceof Promise) {
try {
res = await pending;
} catch (err) {
if (errorHandler === undefined) {
res = handleFetchError(err);
} else {
const errRes = await errorHandler(err);
if (errRes === undefined) return;
res = errRes;
}
}
} else res = pending;
await writeResponse(res, outgoing, head);
}
/** A Response the package writes from its cache without waiting: still
* exactly its constructor arguments, with no body or a string or bytes
* body. */
function immediateCacheable(res: Response): boolean {
const light = scriptcResponseLight(res);
return light[0] !== "0" && Number(light[2]!) >= -1;
}
function handleFetchError(e: unknown): Response {
return emptyResponse(e instanceof Error && e.name === "TimeoutError" ? 504 : 500);
}
async function handleRequest(
fetchCallback: FetchCallback,
options: RequestListenerOptions,
incoming: IncomingMessage,
outgoing: ServerResponse,
): Promise<void> {
let req: Request | undefined = undefined;
let res: Response | Promise<Response> | undefined = undefined;
const state: IncomingState = { ended: false, draining: false };
let needsBodyCleanup = false;
let closeHandlerAttached = false;
const ensureCloseHandler = (): void => {
if (req === undefined || closeHandlerAttached) return;
closeHandlerAttached = true;
if (needsBodyCleanup)
outgoing.on("close", () => {
drainAfterClose(incoming, state);
});
};
try {
req = newRequest(incoming, options.hostname, state);
needsBodyCleanup =
options.autoCleanupIncoming !== false &&
!(incoming.method === "GET" || incoming.method === "HEAD");
if (needsBodyCleanup)
incoming.on("end", () => {
state.ended = true;
});
res = fetchCallback(req, { incoming, outgoing });
if (!(res instanceof Promise) && immediateCacheable(res)) {
if (needsBodyCleanup)
outgoing.once("finish", () => {
if (!state.ended) drainIncoming(incoming, state);
});
} else ensureCloseHandler();
} catch (e) {
if (res !== undefined) return handleResponseError(e, outgoing);
const errorHandler = options.errorHandler;
if (errorHandler !== undefined) {
ensureCloseHandler();
const handled = await errorHandler(
req !== undefined ? e : e instanceof RequestError ? e : toRequestError(e),
);
if (handled === undefined) return;
res = handled;
} else if (req === undefined) res = emptyResponse(400);
else res = handleFetchError(e);
}
try {
await respond(res, outgoing, options.errorHandler, incoming.method === "HEAD");
} catch (e) {
handleResponseError(e, outgoing);
}
}
function toRequestError(e: unknown): RequestError {
return new RequestError(e instanceof Error ? e.message : String(e), { cause: e });
}
export function getRequestListener(
fetchCallback: FetchCallback,
options: RequestListenerOptions = {},
): (incoming: IncomingMessage, outgoing: ServerResponse) => Promise<void> {
return (incoming, outgoing) => handleRequest(fetchCallback, options, incoming, outgoing);
}
export function createAdaptorServer(options: Options): ServerType {
const listener = getRequestListener(options.fetch, {
hostname: options.hostname,
overrideGlobalObjects: options.overrideGlobalObjects,
autoCleanupIncoming: options.autoCleanupIncoming,
});
return createServer((incoming, outgoing) => {
void listener(incoming, outgoing);
});
}
export function serve(
options: Options,
listeningListener?: (info: AddressInfo) => void,
): ServerType {
const server = createAdaptorServer(options);
server.listen(options.port ?? 3000, options.hostname, () => {
const info = server.address() as AddressInfo;
if (listeningListener !== undefined) listeningListener(info);
});
return server;
}
@@ -19,7 +19,9 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
zlib: features.zlib,
...(features.brotli ? { brotli: true } : {}),
assert: features.assert,
inspect: features.inspect,
dynInvoke: features.dynInvoke,
@@ -50,6 +52,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
dgram: features.dgram,
dnsPromises: features.dnsPromises,
dgramDyn: features.dgramDyn,
textCodecDyn: features.textCodecDyn,
watch: features.fsWatch,
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
nodeTest: features.nodeTest,
@@ -60,6 +63,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
dh: features.dh,
pki: features.pki,
ed25519: features.ed25519,
x509: features.x509,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
};
+26 -24
View File
@@ -3,6 +3,7 @@
import type { DynamicFieldStores } from "../ir/int32-slots.js";
import type { IrExpr, IrModule, IrStmt } from "../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../ir/node-index.js";
import {
computeCycleMutability,
constructorEscapePoint,
@@ -97,7 +98,10 @@ export interface ConstructionFacts {
privateStores: Set<IrStmt>;
}
export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
/** The nodes that can reach a constructor with a reused receiver. */
const CONSTRUCTOR_USE_KINDS: readonly IrNodeKind[] = ["call", "closure"];
export function computeConstructionFacts(mod: IrModule, nodes?: IrNodeIndex): ConstructionFacts {
const facts: ConstructionFacts = { privateStores: new Set() };
if (mod.lib !== undefined) return facts;
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c] as const));
@@ -122,6 +126,7 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
c = classes.get(c)!.base;
}
};
const scan = nodes?.scan(CONSTRUCTOR_USE_KINDS);
for (const fn of mod.functions) {
const own = ctorClass.get(fn.name);
const thisId = own !== undefined ? thisOf(own) : null;
@@ -129,22 +134,19 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
own !== undefined && classes.get(own)!.base !== undefined
? `%${classes.get(own)!.base}.constructor`
: null;
const expr = (e: IrExpr): boolean => {
if (e.kind === "call") {
const target = ctorClass.get(e.callee);
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "call") {
const target = ctorClass.get(node.callee);
if (
target !== undefined &&
!(e.callee === baseCtor && e.args.length > 0 && isThisRef(e.args[0]!, thisId))
!(node.callee === baseCtor && node.args.length > 0 && isThisRef(node.args[0]!, thisId))
)
markReused(target);
} else if (e.kind === "closure") {
const target = ctorClass.get(e.fnName);
} else if (node.kind === "closure") {
const target = ctorClass.get(node.fnName);
if (target !== undefined) markReused(target);
}
return everyExprChild(e, expr, stmt);
};
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
fn.body.every(stmt);
});
}
// The first escaping statement of each constructor (body length: none).
@@ -192,32 +194,32 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
return facts;
}
/** The nodes that write a binding: statement and expression assignments. */
const ASSIGNMENT_KINDS: readonly IrNodeKind[] = ["assign", "assignExpr"];
/** Module constants whose value never changes once initialized: immutable,
* not TDZ-checked (every read follows the initialization), no import-cycle
* init flag, and at most one assignment in the whole module. Such a global
* owns its value for the rest of the program, so a read of it can be
* borrowed across any later operand or call. */
export function stableGlobals(mod: IrModule): Set<string> {
export function stableGlobals(mod: IrModule, nodes?: IrNodeIndex): Set<string> {
const candidates = new Set(
(mod.globals ?? [])
.filter((g) => !g.mutable && g.tdz !== true && g.initFlag === undefined)
.map((g) => g.id),
);
const assigned = new Set<string>();
const expr = (e: IrExpr): boolean => {
if (e.kind === "assignExpr" && candidates.has(e.localId)) candidates.delete(e.localId);
return everyExprChild(e, expr, stmt);
};
const stmt = (s: IrStmt): boolean => {
if (s.kind === "assign" && candidates.has(s.localId)) {
if (assigned.has(s.localId)) candidates.delete(s.localId);
assigned.add(s.localId);
}
return everyStmtChild(s, expr, stmt);
};
const scan = nodes?.scan(ASSIGNMENT_KINDS);
for (const fn of mod.functions) {
if (candidates.size === 0) break;
fn.body.every(stmt);
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "assignExpr" && candidates.has(node.localId))
candidates.delete(node.localId);
else if (node.kind === "assign" && candidates.has(node.localId)) {
if (assigned.has(node.localId)) candidates.delete(node.localId);
assigned.add(node.localId);
}
});
}
return candidates;
}
@@ -54,6 +54,7 @@ export async function compilePackedLibrary(
dgram: false,
dnsPromises: false,
dgramDyn: false,
textCodecDyn: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -63,6 +64,7 @@ export async function compilePackedLibrary(
dh: false,
pki: false,
ed25519: false,
x509: options.x509 ?? false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
@@ -151,7 +153,7 @@ export async function compilePackedLibrary(
...(await Promise.all(
shards
.slice(start, start + 4)
.map((shard, offset) => emit(shard.source, `shard-${start + offset}.o`)),
.map((shard, offset) => emit(shard.source(), `shard-${start + offset}.o`)),
)),
);
}
+13 -1
View File
@@ -21,6 +21,7 @@ import {
import { type NativeArtifactDependency } from "./native/contracts.js";
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
import type { NativeLinkPlan } from "./link-plan.js";
import { lldLinkerArgs } from "./lld.js";
import {
createDarwinDebugSymbols,
installDarwinDebugSymbols,
@@ -240,8 +241,11 @@ export async function linkNativeExecutable(
try {
const staged = await stageRuntimePackArtifacts(plan.runtimePack);
stagedRoot = staged.root;
// Linux links with lld when the default driver finds it.
const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : [];
const args = [
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
...lld,
...plan.driverFlags,
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
...plan.systemLibraries.map((name) => `-l${name}`),
@@ -276,7 +280,15 @@ export async function linkNativeExecutable(
...(await snapshotDependencies([...toolchain, ...additionalDependencyPaths])),
])
.catch(() => null);
await execFileAsync(linker, args);
await execFileAsync(linker, args).catch(async (error: unknown) => {
// A failed link with lld runs again with the driver's default linker,
// whose error (if any) is the one reported.
if (lld.length === 0) throw error;
await execFileAsync(
linker,
args.filter((arg) => !lld.includes(arg)),
);
});
const output = await stat(privateOut);
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
if (plan.darwinDebugSymbols) {
+61
View File
@@ -0,0 +1,61 @@
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, test } from "vitest";
import { driverFindsLld, lldLinkerArgs } from "./lld.js";
const directories: string[] = [];
afterEach(() => {
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
});
function tool(path: string): string {
writeFileSync(path, "#!/bin/sh\n");
chmodSync(path, 0o755);
return path;
}
function fixture() {
const root = mkdtempSync(join(tmpdir(), "scriptc-lld-"));
directories.push(root);
const llvm = join(root, "llvm", "bin");
const usr = join(root, "usr", "bin");
const other = join(root, "other");
for (const directory of [llvm, usr, other]) mkdirSync(directory, { recursive: true });
return { root, llvm, usr, other };
}
test.skipIf(process.platform === "win32")(
"a driver finds ld.lld beside itself, beside its real executable, or on PATH",
() => {
const f = fixture();
const clang = tool(join(f.llvm, "clang"));
// Without ld.lld anywhere.
expect(driverFindsLld(clang, { PATH: f.other })).toBe(false);
// Beside the driver.
tool(join(f.llvm, "ld.lld"));
expect(driverFindsLld(clang, { PATH: f.other })).toBe(true);
// A PATH entry that links to the driver: clang looks beside its real executable.
symlinkSync(clang, join(f.usr, "clang"));
expect(driverFindsLld("clang", { PATH: f.usr })).toBe(true);
// A driver elsewhere finds ld.lld on PATH, and not otherwise.
const gcc = tool(join(f.other, "gcc"));
expect(driverFindsLld(gcc, { PATH: f.other })).toBe(false);
expect(driverFindsLld(gcc, { PATH: [f.other, f.llvm].join(":") })).toBe(true);
expect(driverFindsLld("missing-driver", { PATH: f.llvm })).toBe(false);
},
);
test.skipIf(process.platform === "win32")(
"Linux links with the default driver use lld when the driver finds it",
() => {
const f = fixture();
const clang = tool(join(f.llvm, "clang"));
tool(join(f.llvm, "ld.lld"));
const env = { PATH: f.other };
expect(lldLinkerArgs("linux", clang, env)).toEqual(["-fuse-ld=lld"]);
expect(lldLinkerArgs("darwin", clang, env)).toEqual([]);
expect(lldLinkerArgs("windows", clang, env)).toEqual([]);
expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]);
},
);
+49
View File
@@ -0,0 +1,49 @@
import { realpathSync, statSync } from "node:fs";
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
function isFile(path: string): boolean {
try {
return statSync(path).isFile();
} catch {
return false;
}
}
/** The executable `command` names: a path, or the first match on PATH. */
function commandFile(command: string, env: NodeJS.ProcessEnv): string | null {
if (command.includes("/")) return isFile(command) ? resolve(command) : null;
for (const directory of (env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) {
const candidate = join(directory || ".", command);
if (isFile(candidate)) return isAbsolute(candidate) ? candidate : resolve(candidate);
}
return null;
}
/**
* Whether the compiler driver `driver` finds ld.lld for `-fuse-ld=lld`: in
* the directory of the driver's executable (where clang looks first) or on
* PATH. Linking still falls back to the driver's default linker when a link
* with lld fails.
*/
export function driverFindsLld(driver: string, env: NodeJS.ProcessEnv = process.env): boolean {
const file = commandFile(driver, env);
if (file === null) return false;
try {
for (const directory of new Set([dirname(file), dirname(realpathSync(file))]))
if (isFile(join(directory, "ld.lld"))) return true;
} catch {
return false;
}
return commandFile("ld.lld", env) !== null;
}
/** `-fuse-ld=lld` for Linux links whose driver finds ld.lld. */
export function lldLinkerArgs(
platform: string,
driver: string,
env: NodeJS.ProcessEnv = process.env,
): string[] {
return platform === "linux" && env["SCRIPTC_LINKER"] === undefined && driverFindsLld(driver, env)
? ["-fuse-ld=lld"]
: [];
}
+65 -12
View File
@@ -459,18 +459,72 @@ export type FreeObservers = "weak" | "weak-or-source";
* type's disposals), and the block — `scr_cyc_free` for collector-headered
* objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines
* end in an open block.
*
* With `keep` (an alloca of ptr holding NULL; inline allocation paths only),
* a block that would go onto the calling thread's free list is stored there
* instead, for a teardown segment (emitSegmentFree); a block the system or
* another arena owns is still freed.
*/
export function emitObjectFree(
host: AllocHost,
cyc: boolean,
observers: FreeObservers = "weak",
keep: string | null = null,
): string[] {
const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers));
const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers, keep));
if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE);
host.declare(`declare void @scr_obj_free_note()`);
return lines;
}
/** Lines that put the blocks `first` to `last` (linked through their first
* words; one block when they are equal) of the class at reservation offset
* `off` on the calling thread's free list. */
function freeListPushLines(prefix: string, off: string, first: string, last: string): string[] {
return [
` %${prefix}.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`,
` %${prefix}.sh32 = load i32, ptr %${prefix}.shp`,
` %${prefix}.sh = zext i32 %${prefix}.sh32 to i64`,
` %${prefix}.c = lshr i64 ${off}, %${prefix}.sh`,
` %${prefix}.c8 = shl i64 %${prefix}.c, 3`,
` %${prefix}.fo = add i64 %${prefix}.c8, ${SA_FREE}`,
` %${prefix}.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %${prefix}.fo`,
` %${prefix}.head = load ptr, ptr %${prefix}.fp`,
` store ptr %${prefix}.head, ptr ${last}`,
` store ptr ${first}, ptr %${prefix}.fp`,
];
}
/**
* Lines that free a teardown segment: the blocks `first` to `last` (ptr
* values, both NULL for an empty segment), linked through their first words,
* which emitObjectFree kept. Every block of a segment is in the calling
* thread's arena and of one size class, so the segment goes onto that
* class's free list in one step, and the next allocations of the class take
* its blocks in segment order. Labels use `prefix`; the lines end in the
* open block `${prefix}.done`.
*/
export function emitSegmentFree(
host: AllocHost,
prefix: string,
first: string,
last: string,
): string[] {
declareState(host);
const lines = [
` %${prefix}.none = icmp eq ptr ${first}, null`,
` br i1 %${prefix}.none, label %${prefix}.done, label %${prefix}.push`,
`${prefix}.push:`,
` %${prefix}.base = load i64, ptr @scr_sa`,
` %${prefix}.bi = ptrtoint ptr ${first} to i64`,
` %${prefix}.off = sub i64 %${prefix}.bi, %${prefix}.base`,
...freeListPushLines(prefix, `%${prefix}.off`, first, last),
` br label %${prefix}.done`,
`${prefix}.done:`,
];
return threadLocalState(host, prefix, lines);
}
/** Lines that leave in `%<prefix>.obs` whether the program can observe a
* disposal of this type (folds to a constant). */
function observedLines(prefix: string, observers: FreeObservers): string[] {
@@ -482,8 +536,14 @@ function observedLines(prefix: string, observers: FreeObservers): string[] {
];
}
function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] {
function freeLines(
host: AllocHost,
cyc: boolean,
observers: FreeObservers,
keep: string | null,
): string[] {
if (host.inlineAlloc !== true) {
if (keep !== null) throw new Error("a kept block needs the inline allocation paths");
if (cyc) {
host.declare(`declare void @scr_cyc_free(ptr)`);
return [` call void @scr_obj_free_note()`, ` call void @scr_cyc_free(ptr %o)`];
@@ -548,16 +608,9 @@ function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): str
` %sf.inx = call i1 @llvm.expect.i1(i1 %sf.in, i1 true)`,
` br i1 %sf.inx, label %sf.push, label %sf.sys`,
`sf.push:`,
` %sf.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`,
` %sf.sh32 = load i32, ptr %sf.shp`,
` %sf.sh = zext i32 %sf.sh32 to i64`,
` %sf.c = lshr i64 %sf.off, %sf.sh`,
` %sf.c8 = shl i64 %sf.c, 3`,
` %sf.fo = add i64 %sf.c8, ${SA_FREE}`,
` %sf.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %sf.fo`,
` %sf.head = load ptr, ptr %sf.fp`,
` store ptr %sf.head, ptr ${blk}`,
` store ptr ${blk}, ptr %sf.fp`,
...(keep !== null
? [` store ptr ${blk}, ptr ${keep}`]
: freeListPushLines("sf", "%sf.off", blk, blk)),
` br label %sf.done`,
`sf.sys: ; outside the reservation, or the allocator is compiled out`,
` call void @scr_obj_free_note()`,
@@ -10,7 +10,7 @@
* behavior. */
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
const APPENDING_METHODS: ReadonlySet<string> = new Set([
"push",
@@ -30,7 +30,27 @@ function isEmptyLiteral(e: IrExpr): boolean {
);
}
export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr> {
/** The nodes that name an appended binding or field, and the stores that
* initialize one. */
const TARGET_KINDS: readonly IrNodeKind[] = [
"arrIntrinsic",
"assignExpr",
"arraySet",
"varDecl",
"assign",
];
const STORE_KINDS: readonly IrNodeKind[] = [
"assignExpr",
"varDecl",
"assign",
"fieldSet",
"recordSet",
];
export function findAppendedArrayLiterals(
module: IrModule,
index?: IrNodeIndex,
): ReadonlySet<IrExpr> {
// Bindings are function-scoped by id; module globals are shared.
const localsOf = new Map<string, ReadonlySet<string>>();
for (const fn of module.functions) {
@@ -65,20 +85,15 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr>
const field = fieldOf(target);
if (field !== null) appendedFields.add(field);
};
const targets = index?.scan(TARGET_KINDS);
for (const fn of module.functions) {
everyStmtList(fn.body, {
expr: (e) => {
if (e.kind === "arrIntrinsic" && APPENDING_METHODS.has(e.method))
noteTarget(fn.name, e.receiver);
else if (e.kind === "assignExpr") noteAlias(fn.name, e.localId, e.value);
return true;
},
stmt: (s) => {
if (s.kind === "arraySet") noteTarget(fn.name, s.arr);
else if (s.kind === "varDecl") noteAlias(fn.name, s.localId, s.init);
else if (s.kind === "assign") noteAlias(fn.name, s.localId, s.value);
return true;
},
eachNodeOfKinds(fn, targets, (node) => {
if (node.kind === "arrIntrinsic" && APPENDING_METHODS.has(node.method))
noteTarget(fn.name, node.receiver);
else if (node.kind === "assignExpr") noteAlias(fn.name, node.localId, node.value);
else if (node.kind === "arraySet") noteTarget(fn.name, node.arr);
else if (node.kind === "varDecl") noteAlias(fn.name, node.localId, node.init);
else if (node.kind === "assign") noteAlias(fn.name, node.localId, node.value);
});
}
for (const binding of appendedBindings)
@@ -89,24 +104,52 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr>
const consider = (value: IrExpr | null, appended: boolean): void => {
if (value && appended && isEmptyLiteral(value)) result.add(value);
};
const stores = index?.scan(STORE_KINDS);
for (const fn of module.functions) {
const binding = (localId: string): boolean =>
appendedBindings.has(bindingKey(fn.name, localId));
everyStmtList(fn.body, {
expr: (e) => {
if (e.kind === "assignExpr") consider(e.value, binding(e.localId));
return true;
},
stmt: (s: IrStmt) => {
if (s.kind === "varDecl") consider(s.init, binding(s.localId));
else if (s.kind === "assign") consider(s.value, binding(s.localId));
else if (s.kind === "fieldSet")
consider(s.value, appendedFields.has(fieldKey(s.className, s.field)));
else if (s.kind === "recordSet")
consider(s.value, appendedFields.has(fieldKey(s.shapeId, s.field)));
return true;
},
eachNodeOfKinds(fn, stores, (node) => {
if (node.kind === "assignExpr") consider(node.value, binding(node.localId));
else if (node.kind === "varDecl") consider(node.init, binding(node.localId));
else if (node.kind === "assign") consider(node.value, binding(node.localId));
else if (node.kind === "fieldSet")
consider(node.value, appendedFields.has(fieldKey(node.className, node.field)));
else if (node.kind === "recordSet")
consider(node.value, appendedFields.has(fieldKey(node.shapeId, node.field)));
});
}
return result;
}
/** `const xs = []` directly followed by a capacity hint for `xs` (an
* `arraySetLength` with `reserve`) whose length reads only literals and
* other locals: the literal can be constructed with that capacity, and the
* hint then has nothing left to do. */
export function reservedArrayLength(
stmts: readonly IrStmt[],
i: number,
): { literal: IrExpr; hint: IrStmt & { kind: "arraySetLength" } } | null {
const decl = stmts[i];
const next = stmts[i + 1];
if (decl?.kind !== "varDecl" || decl.init === null || !isEmptyLiteral(decl.init)) return null;
if (next?.kind !== "arraySetLength" || next.reserve !== true) return null;
if (next.arr.kind !== "varRef" || next.arr.localId !== decl.localId) return null;
return plainLength(next.length, decl.localId) ? { literal: decl.init, hint: next } : null;
}
function plainLength(e: IrExpr, array: string): boolean {
switch (e.kind) {
case "numLit":
return true;
case "varRef":
return e.localId !== array;
case "bin":
return plainLength(e.left, array) && plainLength(e.right, array);
case "ternary":
return (
plainLength(e.cond, array) && plainLength(e.then, array) && plainLength(e.else_, array)
);
default:
return false;
}
}
+13 -1
View File
@@ -9,6 +9,7 @@
* code after it); here the lines are DROPPED instead, and emitStmts skips
* whole statements once the current block is terminated, so no dropped
* definition can leak into a later block. */
import { UNWIND_RESUME, UNWIND_SLOT } from "./unwind-abi.js";
export class BlockBuilder {
private readonly blocks: { label: string; lines: string[]; term: string | null }[] = [];
@@ -86,7 +87,18 @@ export class BlockBuilder {
terminate(s: string): void {
if (this.cur.term === null) {
if (this.returnEpilogue && s.startsWith("ret ")) this.line(this.returnEpilogue);
if (
this.returnEpilogue &&
(s.startsWith("ret ") ||
s.startsWith("call void @scr_unwind_raise()") ||
s === UNWIND_RESUME)
)
this.line(this.returnEpilogue);
if (s === UNWIND_RESUME) {
const exception = this.tmp();
this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`);
s = `resume { ptr, i32 } ${exception}`;
}
this.cur.term = ` ${this.withDebugLocation(s)}`;
}
}
@@ -7,13 +7,21 @@ import type { LlvmEmitterContext } from "./expr-context.js";
* buffer. Neither overlaps the other, a native field, array storage or a
* module global's slot, so element stores leave header loads and global
* receiver slots loop-invariant. Untagged accesses and calls still alias
* every category. */
* every category.
*
* Header loads are also `!invariant.load`: no store or call between the
* construction of a view and its release changes them. Without it, a data
* pointer load ahead of a bounds branch is sunk into the in-bounds block,
* and LICM keeps a conditionally executed load inside the loop. */
export function bytesHeaderAlias(host: LlvmEmitterContext): string {
return host.memoryAliasAttachment("bytes:header");
const tag = host.memoryAliasAttachment("bytes:header");
return tag === "" ? "" : `${tag}, !invariant.load !{}`;
}
/** Element storage's tag, and the alias scope of the access being emitted
* in a loop version's fast copy (bytesAccessScope). */
export function bytesElementAlias(host: LlvmEmitterContext): string {
return host.memoryAliasAttachment("bytes:elements");
return host.memoryAliasAttachment("bytes:elements") + host.bytesAccessScope;
}
/** A module global's slot is its own category for whole-slot loads of a
@@ -14,11 +14,25 @@ interface Bound extends Extent {
minimum: number;
}
type Node = IrExpr | IrStmt;
const FLIPPED: Readonly<Record<string, string>> = {
"<": ">",
"<=": ">=",
">": "<",
">=": "<=",
"===": "===",
"==": "==",
"!==": "!==",
"!=": "!=",
};
/** Relational proofs for fixed typed-array lengths. Cached lengths retain
* their receiver identity only when that binding is never reassigned.
* Loops may use offsets in either direction, but both index integrality
* and receiver stability are established separately from the relation. */
* and receiver stability are established separately from the relation.
* For loops hold one bound over the body. While loops start from the
* condition's bound and follow the body in order: increments raise it and
* comparisons with the length in if conditions narrow it. Indexes are
* `counter + c` or `L + d - counter`, where L is the receiver's length. */
export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): ReadonlySet<Node> {
const proved = new Map<Node, boolean>();
if (fn.async || fn.generator || !fn.locals.some((local) => local.type.kind === "bytes"))
@@ -96,6 +110,20 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
const result = left + (e.op === "+" ? e.right.value : -e.right.value);
return Number.isSafeInteger(result) ? result : null;
}
/** For an index of the form `L + d - counter` (`len - 1 - i`, `right - i`
* with `right = len - 1`), where L is the receiver's length: d. */
function reversed(e: IrExpr, bound: Bound): number | null {
if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-")) return null;
if (e.op === "-" && e.right.kind === "varRef" && e.right.localId === bound.counter) {
const length = extent(e.left, false);
return length && length.receiver === bound.receiver ? length.offset : null;
}
if (e.right.kind !== "numLit" || !Number.isSafeInteger(e.right.value)) return null;
const left = reversed(e.left, bound);
if (left === null) return null;
const result = left + (e.op === "+" ? e.right.value : -e.right.value);
return Number.isSafeInteger(result) ? result : null;
}
function affineRange(e: IrExpr, bound: Bound): IntegerRange | null {
if (e.kind === "varRef" && e.localId === bound.counter) {
const max = Number.MAX_SAFE_INTEGER + bound.offset - 1;
@@ -159,7 +187,7 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
if (s.cond.left.kind !== "varRef" || s.cond.left.localId !== counter) {
if (right.kind !== "varRef" || right.localId !== counter) return null;
right = s.cond.left;
op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[op] ?? "";
op = FLIPPED[op] ?? "";
}
const update = s.update;
let step = 0;
@@ -212,6 +240,98 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
}
return result && stable(s.body, result.receiver, counter) ? result : null;
}
/** A comparison of the counter with the receiver's length: the operator
* with the counter on the left, and the length's offset. */
function lengthComparison(e: IrExpr, bound: Bound): { op: string; offset: number } | null {
if (e.kind !== "bin") return null;
let other = e.right,
op: string = e.op;
if (e.left.kind !== "varRef" || e.left.localId !== bound.counter) {
if (other.kind !== "varRef" || other.localId !== bound.counter) return null;
other = e.left;
op = FLIPPED[op] ?? op;
}
const length = extent(other, false);
return length && length.receiver === bound.receiver ? { op, offset: length.offset } : null;
}
/** The step of `counter = counter + c` with a positive literal c, or null. */
function increment(s: IrStmt, counter: string): number | null {
return s.kind === "assign" &&
s.localId === counter &&
s.value.kind === "bin" &&
s.value.op === "+" &&
s.value.left.kind === "varRef" &&
s.value.left.localId === counter &&
s.value.right.kind === "numLit" &&
Number.isSafeInteger(s.value.right.value) &&
s.value.right.value > 0
? s.value.right.value
: null;
}
function writes(s: IrStmt, id: string): boolean {
return !everyStmtList([s], {
expr: (e) => (e.kind !== "assignExpr" && e.kind !== "incDec") || e.localId !== id,
stmt: (child) =>
!(
((child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl") &&
child.localId === id) ||
(child.kind === "tryCatch" && child.catchLocalId === id)
),
});
}
/** A while loop whose condition bounds an integer counter by a typed
* array's length. The body may write the counter only by statements
* that add a positive literal, and may not write the receiver. Calls
* cannot write either binding (both are uncaptured locals), and a typed
* array's length is fixed at construction, so the body may hold calls. */
function whileBound(s: IrStmt & { kind: "while" }): Bound | null {
const cond = s.cond;
if (cond.kind !== "bin") return null;
const counterRef = [cond.left, cond.right].find(
(side, i) =>
side.kind === "varRef" &&
eligible(side.localId) &&
extent(i === 0 ? cond.right : cond.left, false) !== null,
);
const minimum = counterRef ? ranges.get(counterRef)?.min : undefined;
if (counterRef?.kind !== "varRef" || minimum === undefined) return null;
const counter = counterRef.localId;
const length = extent(counterRef === cond.left ? cond.right : cond.left, false)!;
const receiver = length.receiver;
const comparison = lengthComparison(cond, { receiver, counter, offset: 0, minimum });
if (comparison?.op !== "<" && comparison?.op !== "<=") return null;
// An increment is a statement of the body: one inside an expression
// (a comma expression's statements) would run where flow does not
// follow it.
const writesOk = everyStmtList(s.body, {
expr: (e) =>
((e.kind !== "assignExpr" && e.kind !== "incDec") ||
(e.localId !== counter && e.localId !== receiver)) &&
everyExprChild(
e,
() => true,
(child) => !writes(child, counter),
),
stmt: (child) => {
if (child.kind === "assign" && child.localId === counter)
return increment(child, counter) !== null;
if (child.kind === "assign" || child.kind === "forOf" || child.kind === "varDecl")
return child.localId !== counter && child.localId !== receiver;
return (
child.kind !== "tryCatch" ||
(child.catchLocalId !== counter && child.catchLocalId !== receiver)
);
},
});
return writesOk
? {
receiver,
counter,
offset: comparison.offset + (comparison.op === "<=" ? 1 : 0),
minimum,
}
: null;
}
function access(node: Node, array: IrExpr, index: IrExpr, active: Bound[], width: number): void {
const integer = ranges.get(index);
let safe = false;
@@ -228,6 +348,13 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
safe = true;
break;
}
// counter < L + bound.offset and counter >= bound.minimum put
// L + d - counter in [0, L - width].
const d = integer ? reversed(index, bound) : null;
if (d !== null && bound.offset <= d + 1 && bound.minimum >= d + width) {
safe = true;
break;
}
}
}
proved.set(node, (proved.get(node) ?? true) && safe);
@@ -244,8 +371,84 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
(child) => stmt(child, active),
);
}
/** The bounds where `cond` is true and where it is false, from the
* bound before it. */
function narrow(cond: IrExpr, bound: Bound): [Bound, Bound] {
const comparison = lengthComparison(cond, bound);
if (!comparison) return [bound, bound];
const j = comparison.offset,
offset = bound.offset;
// counter < L + j, counter <= L + j, and counter !== L + j.
const below = { ...bound, offset: Math.min(offset, j) },
atMost = { ...bound, offset: Math.min(offset, j + 1) },
unequal = offset === j + 1 ? { ...bound, offset: j } : bound;
switch (comparison.op) {
case "<":
return [below, bound];
case "<=":
return [atMost, bound];
case ">":
return [bound, atMost];
case ">=":
return [bound, below];
case "===":
case "==":
return [atMost, unequal];
case "!==":
case "!=":
return [unequal, atMost];
default:
return [bound, bound];
}
}
/** Visits a while body in order, from the bound that holds on entry.
* Returns the bound at the end of the list. Its offset is -Infinity (and
* its minimum Infinity) when the end is unreachable, and Infinity when
* nothing bounds the counter there. Every write of the counter adds a
* positive literal (whileBound), so the minimum survives any write. */
function flow(list: IrStmt[], active: Bound[], bound: Bound): Bound {
for (const s of list) {
const current = Number.isFinite(bound.offset) ? [...active, bound] : active;
const step = increment(s, bound.counter);
if (step !== null) {
bound = { ...bound, offset: bound.offset + step, minimum: bound.minimum + step };
} else if (s.kind === "if") {
expr(s.cond, current);
const [then, else_] = narrow(s.cond, bound);
const left = flow(s.then, active, then),
right = s.else_ ? flow(s.else_, active, else_) : else_;
bound = {
...bound,
offset: Math.max(left.offset, right.offset),
minimum: Math.min(left.minimum, right.minimum),
};
} else if (s.kind === "block" && !s.labels?.length) {
bound = flow(s.body, active, bound);
} else if (writes(s, bound.counter)) {
stmt(s, active);
bound = { ...bound, offset: Infinity };
} else {
stmt(s, current);
if (
s.kind === "break" ||
s.kind === "continue" ||
s.kind === "return" ||
s.kind === "throw"
)
bound = { ...bound, offset: -Infinity, minimum: Infinity };
}
}
return bound;
}
function stmt(s: IrStmt, active: Bound[]): boolean {
if (s.kind === "bytesSet") access(s, s.arr, s.index, active, 1);
if (s.kind === "while") {
expr(s.cond, active);
const bound = whileBound(s);
if (!bound) return s.body.every((child) => stmt(child, active));
flow(s.body, active, bound);
return true;
}
if (s.kind === "for") {
if (s.init) stmt(s.init, active);
if (s.cond) expr(s.cond, active);
@@ -11,7 +11,7 @@ import {
type IrType,
} from "../../ir/ir.js";
import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
import { matchIntegerCountedForLoop } from "../../ir/integer-loops.js";
import { matchIntegerCountedForLoop, matchIntegerPairedForLoop } from "../../ir/integer-loops.js";
import { findInitializedByteLoopBindings } from "./byte-windows.js";
import { matchBytesLoopVersion, type BytesLoopVersion } from "./bytes-loop-versions.js";
import { emitLlvmModule } from "./emitter.js";
@@ -249,6 +249,11 @@ test("emitted loops version the body and tag typed-array memory", () => {
const elements = /store double [^\n]*!tbaa !(\d+)/.exec(body)![1];
const header = /load ptr, ptr [^\n]*!tbaa !(\d+)/.exec(body)![1];
expect(elements).not.toBe(header);
// Header loads are invariant for the view's lifetime; element accesses
// are not.
expect(body).toMatch(/load ptr, ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m);
expect(body).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+, !invariant\.load !\{\}$/m);
expect(body).not.toMatch(/(load double|store double)[^\n]*invariant\.load/);
});
test("loops to a typed array's length version the other typed arrays they index", () => {
@@ -380,7 +385,10 @@ test("while loop counters below a bound version their accesses", () => {
]);
const declared = (loop: IrStmt): IrFunction =>
fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => {
const versionOf = (
loop: IrStmt & { kind: "while" },
workers = false,
): BytesLoopVersion | null => {
const f = declared(loop);
const ranges = analyzeIntegerRanges(f);
return matchBytesLoopVersion(loop, null, {
@@ -391,7 +399,7 @@ test("while loop counters below a bound version their accesses", () => {
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
proven: new Set(),
scalarStrings: new Set(),
workers: false,
workers,
owned: new Set(),
});
};
@@ -408,6 +416,11 @@ test("while loop counters below a bound version their accesses", () => {
// An access after the counter advances in the same pass stays checked.
const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]);
expect(versionOf(advanced)).toBeNull();
// In a worker program, a while loop versions for its counter's accesses,
// and not for thread-owned receivers alone.
expect(versionOf(copy, true)!.ownedReceivers).toHaveLength(2);
const job = whileLoop([set(ref("out", f64s), ref("k"), get(ref("src", f64s), ref("k"))), step]);
expect(versionOf(job, true)).toBeNull();
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
@@ -567,7 +580,9 @@ test("the guard tests a string's ASCII bit and the fast copy reads bytes", () =>
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m));
expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/);
const fast = body.slice(body.lastIndexOf("bytes.version.fast"));
// The byte copy, up to the units copy that follows it.
const units = body.search(/^bytes\.version\.fast\.units\d+:/m);
const fast = body.slice(body.search(/^bytes\.version\.fast\d+:/m), units < 0 ? undefined : units);
expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
expect(fast).not.toContain("@sc_str_char_code_at_int");
// The checked copy keeps the inline helper and its fallback.
@@ -618,3 +633,79 @@ test("while loop counters below a string's length version its reads", () => {
expect(version.bounded.map((c) => c.slack)).toEqual([0]);
expect(version.fixed).toEqual([]);
});
test("a two-counter loop versions the typed arrays it indexes at either counter", () => {
// for (let i = 0, j = k; i < j; i++, j--) { const t = out[i]; out[i] = out[j]; out[j] = t; }
const loop: IrStmt & { kind: "for" } = {
kind: "for",
init: {
kind: "block",
body: [
{ kind: "varDecl", localId: "i", init: num(0), loc },
{ kind: "varDecl", localId: "j", init: ref("k"), loc },
],
loc,
},
cond: { kind: "bin", op: "<", left: ref("i"), right: ref("j"), type: BOOL, loc },
update: {
kind: "block",
body: [
{ kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc },
{ kind: "assign", localId: "j", value: bin("-", ref("j"), num(1)), loc },
],
loc,
},
body: [
{ kind: "varDecl", localId: "t", init: get(ref("out", f64s), ref("i")), loc },
set(ref("out", f64s), ref("i"), get(ref("out", f64s), ref("j"))),
set(ref("out", f64s), ref("j"), ref("t")),
// Another index stays checked.
set(ref("src", f64s), bin("+", ref("i"), num(1)), num(0)),
],
loc,
};
const kernel = fn([loop], [local("t", F64)]);
const ranges = analyzeIntegerRanges(kernel);
const locals = new Map(kernel.locals.map((l) => [l.id, l]));
const paired = matchIntegerPairedForLoop(loop, locals, ranges)!;
expect(paired).not.toBeNull();
const version = matchBytesLoopVersion(
loop,
null,
{
locals,
invariantGlobals: new Set(),
captured: new Set(),
ranges,
initialized: findInitializedByteLoopBindings(kernel).get(loop) ?? new Set(),
proven: new Set(),
workers: false,
owned: new Set(),
scalarStrings: new Set(),
},
paired,
)!;
expect(version.paired?.loop).toBe(paired);
expect(version.paired?.receivers.map((r) => version.receivers[r]!.ref)).toEqual([
ref("out", f64s),
]);
expect(version.bounds.size).toBe(4);
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
kernel,
],
};
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
// `i` starts at the literal 0, so its test compares the integer itself.
expect(guard).toMatch(/icmp sge i64 0, 0/);
expect(guard).toMatch(/icmp slt i64 %\S+, %\S+/);
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
// The fast copy keeps only the `src[i + 1]` check, and no second guard.
expect(fast.match(/bytes\.index\.invalid\d+:/g)).toHaveLength(1);
expect(fast).not.toContain("counted.fast");
});
@@ -1,14 +1,32 @@
import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
import {
BYTES_ELEMENT_SIZE,
F64,
type IrExpr,
type IrLocal,
type IrStmt,
type IrType,
} from "../../ir/ir.js";
import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js";
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
import {
emitCountedLoopLimit,
emitCountedLoopSafety,
emitExactIntegerTest,
emitStartIntegerTest,
} from "./counted-loops.js";
import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
import { f64Lit } from "./common.js";
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
import {
emitStringAsciiFlag,
emitStringByteLength,
STRING_SCAN_UNITS_DECL,
stringScanType,
} from "./string-reads.js";
import { unitSafeLoop } from "./string-unit-versions.js";
type Node = IrExpr | IrStmt;
type VarRef = IrExpr & { kind: "varRef" };
@@ -69,6 +87,14 @@ export interface BytesLoopVersion {
receivers: { ref: VarRef; global: boolean; string: boolean }[];
/** Receivers the guard requires to have no shared storage. */
ownedReceivers: number[];
/** The string reads among `bounds`. */
stringReads: ReadonlySet<Node>;
/** The version's one string receiver, when the loop may also run over
* its UTF-16 units if it is not ASCII (string-unit-versions.ts). */
unitsReceiver: number | null;
/** The units receiver's `length` reads in the loop. Both fast copies
* read the length the guard took (its UTF-16 length) in their place. */
unitLengths: ReadonlySet<IrExpr>;
fixed: FixedCheck[];
affine: AffineCheck[];
/** While loops: accesses at a counter below a loop-invariant bound. */
@@ -79,11 +105,21 @@ export interface BytesLoopVersion {
guarded: ReadonlySet<string>;
/** The counted loop whose iteration space the affine checks cover. */
counted: IntegerCountedForLoop | null;
/** A two-counter loop (`for (let i = lo, j = hi; i < j; i++, j--)`) and
* the receivers it indexes at either counter. Both counters stay between
* the two starts, so the guard requires `lo >= 0` and `hi < length`. */
paired: { loop: IntegerPairedForLoop; receivers: number[] } | null;
/** The guard also requires every counter value within [-bound, bound]. */
counterBound: number | null;
/** Integer ranges of affine index operands that hold in the unchecked
* loop (where the guard bounds the counter). */
ranges: ReadonlyMap<IrExpr, IntegerRange>;
/** Typed-array receivers whose element ranges the guard proves disjoint,
* and the receiver of each of their element reads and writes. The fast
* copy gives each receiver its own alias scope, so a store to one view
* leaves the elements of the others in registers. Null when the loop
* writes no element, or reads and writes a single view. */
scopes: { receivers: number[]; nodes: ReadonlyMap<Node, number> } | null;
}
export interface BytesLoopFacts {
@@ -106,11 +142,16 @@ export interface BytesLoopFacts {
/** String bindings held as a slice of another string (string-slices.ts):
* they have no string header for the guard to read. */
scalarStrings: ReadonlySet<string>;
/** The functions a loop over a string's UTF-16 units may call; absent
* or null where emitted code does not read the runtime's scan receiver. */
unitSafeFunctions?: ReadonlySet<string> | null;
}
const MAX_COST = 400;
const MAX_FIXED_END = 2 ** 31;
const MAX_CHECKS = 8;
/** Alias-scoped views per loop: the guard tests every pair's ranges. */
const MAX_SCOPED_RECEIVERS = 6;
const MAX_OFFSET = 2 ** 31;
const SAFE = Number.MAX_SAFE_INTEGER;
/** The range the guard requires of an invariant index term without a
@@ -153,6 +194,7 @@ export function matchBytesLoopVersion(
loop: IrStmt & { kind: "for" | "while" },
counted: IntegerCountedForLoop | null,
facts: BytesLoopFacts,
paired: IntegerPairedForLoop | null = null,
): BytesLoopVersion | null {
const written = new Set<string>();
const temps = new Map<string, IrExpr>();
@@ -438,6 +480,9 @@ export function matchBytesLoopVersion(
string,
{ check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] }
>();
const pairedCounters =
paired !== null && counted === null ? new Set([paired.low.localId, paired.high.localId]) : null;
const pairedNodes = new Map<number, Node[]>();
const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => {
const ref = root(array);
if (!ref) return;
@@ -448,6 +493,20 @@ export function matchBytesLoopVersion(
else owned.set(receiver, [node]);
}
if (facts.proven.has(node)) return;
// An element at either counter of a two-counter loop.
if (
pairedCounters !== null &&
width === 1 &&
ref.type.kind === "bytes" &&
index.kind === "varRef" &&
pairedCounters.has(index.localId)
) {
const receiver = receiverOf(ref);
const nodes = pairedNodes.get(receiver);
if (nodes) nodes.push(node);
else pairedNodes.set(receiver, [node]);
return;
}
const shape = counter !== null ? affine(index) : null;
if (shape && shape.counter !== 0) {
const operands = new Set<IrExpr>();
@@ -504,11 +563,24 @@ export function matchBytesLoopVersion(
group.nodes.push(node);
} else fixed.set(receiver, { check: { receiver, end }, nodes: [node] });
};
// Element reads and writes of each typed-array receiver, for alias scopes.
const scoped = new Map<Node, number>();
const writtenViews = new Set<number>();
const scope = (node: Node, array: IrExpr, write: boolean): void => {
const ref = root(array);
if (!ref || ref.type.kind !== "bytes") return;
const receiver = receiverOf(ref);
scoped.set(node, receiver);
if (write) writtenViews.add(receiver);
};
const visitExpr = (e: IrExpr): boolean => {
if (e.kind === "bytesIntrinsic") {
const numeric = byteNumberAccess(e);
if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width);
else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1);
else if (e.method === "get" && e.args[0]) {
access(e, e.receiver, e.args[0], 1);
scope(e, e.receiver, false);
}
} else if (
e.kind === "strIntrinsic" &&
(e.method === "charCodeAt" || e.method === "charAt") &&
@@ -518,7 +590,10 @@ export function matchBytesLoopVersion(
return everyExprChild(e, visitExpr, visitStmt);
};
const visitStmt = (s: IrStmt): boolean => {
if (s.kind === "bytesSet") access(s, s.arr, s.index, 1);
if (s.kind === "bytesSet") {
access(s, s.arr, s.index, 1);
scope(s, s.arr, true);
}
return everyStmtChild(s, visitExpr, visitStmt);
};
loop.body.every(visitStmt);
@@ -543,13 +618,23 @@ export function matchBytesLoopVersion(
Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length),
);
const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS);
const pairedKept = [...pairedNodes.entries()].slice(
0,
Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length - boundedKept.length),
);
if (
fixedKept.length === 0 &&
affineKept.length === 0 &&
boundedKept.length === 0 &&
ownedKept.length === 0
ownedKept.length === 0 &&
pairedKept.length === 0
)
return null;
// A while loop versions only for accesses at its counters. Its other
// checks (thread-owned receivers, static indices) save a test or two per
// pass, while a second copy of its body doubles the calls it makes, which
// can keep a worker's job loop from inlining the function that runs a job.
if (loop.kind === "while" && boundedKept.length === 0) return null;
const bounds = new Set<Node>();
const ownedNodes = new Set<Node>();
const used = new Set<number>();
@@ -616,6 +701,10 @@ export function matchBytesLoopVersion(
if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE });
}
}
for (const [receiver, nodes] of pairedKept) {
used.add(receiver);
for (const node of nodes) bounds.add(node);
}
const counterChecks: CounterCheck[] = [];
for (const [id, min] of counterMins) {
const info = whileAccesses!.counters.get(id)!;
@@ -627,9 +716,33 @@ export function matchBytesLoopVersion(
: [],
});
}
// Alias scopes: at least two views, one of them written, each read only
// through plain loads (in a worker program, a view the guard proves
// thread-owned).
const scopedReceivers = [...new Set(scoped.values())].sort((a, b) => a - b);
const ownedSet = new Set(ownedKept.map(([receiver]) => receiver));
const scoping =
scopedReceivers.length >= 2 &&
scopedReceivers.length <= MAX_SCOPED_RECEIVERS &&
scopedReceivers.some((receiver) => writtenViews.has(receiver)) &&
(!facts.workers || scopedReceivers.every((receiver) => ownedSet.has(receiver)));
if (scoping) for (const receiver of scopedReceivers) used.add(receiver);
// Renumber the receivers the kept checks use.
const order = [...used].sort((a, b) => a - b);
const renumber = new Map(order.map((old, index) => [old, index]));
const stringReads = new Set<Node>();
for (const group of [...fixedKept, ...affineKept])
if (receivers[group.check.receiver]!.string)
for (const node of group.nodes) stringReads.add(node);
for (const group of boundedKept)
if (receivers[group.check.receiver]!.string)
for (const { node } of group.nodes) stringReads.add(node);
const strings = order.filter((index) => receivers[index]!.string);
const unitLengths =
facts.unitSafeFunctions != null && strings.length === 1
? unitSafeLoop(loop, receivers[strings[0]!]!.ref.localId, facts.unitSafeFunctions)
: null;
const unitsReceiver = unitLengths !== null ? renumber.get(strings[0]!)! : null;
const needsBound =
affineKept.length > 0 &&
counted !== null &&
@@ -639,6 +752,9 @@ export function matchBytesLoopVersion(
owned: ownedNodes,
receivers: order.map((index) => receivers[index]!),
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
stringReads,
unitsReceiver,
unitLengths: unitLengths ?? new Set<IrExpr>(),
fixed: fixedKept.map((g) => ({
receiver: renumber.get(g.check.receiver)!,
end: g.check.end,
@@ -658,8 +774,18 @@ export function matchBytesLoopVersion(
counters: counterChecks,
guarded,
counted: affineKept.length > 0 ? counted : null,
paired:
paired !== null && pairedKept.length > 0
? { loop: paired, receivers: pairedKept.map(([receiver]) => renumber.get(receiver)!) }
: null,
counterBound: needsBound ? bound : null,
ranges,
scopes: scoping
? {
receivers: scopedReceivers.map((receiver) => renumber.get(receiver)!),
nodes: new Map([...scoped].map(([node, receiver]) => [node, renumber.get(receiver)!])),
}
: null,
};
}
@@ -974,13 +1100,22 @@ function findWhileCounterAccesses(
/** Branches to `fast` when every access the version proves is in bounds for
* every iteration and every view it proves thread-owned has no shared
* storage, and to `slow` otherwise (including a counted loop whose
* integer form is not established, or a module receiver not yet set). */
* integer form is not established, or a module receiver not yet set).
*
* With `unitsFast`, the version's units receiver may instead be a string
* that is not ASCII: the guard has the runtime publish it as the scan
* receiver (one read), tests the same accesses against its UTF-16 length,
* and branches to `unitsFast`. It returns the units pointer that copy
* reads, which stays valid while the loop runs (string-unit-versions.ts),
* and for a version with a units receiver, the receiver's UTF-16 length
* on both fast paths (an i64). */
export function emitBytesLoopVersionGuard(
host: LlvmEmitterContext,
version: BytesLoopVersion,
fast: string,
slow: string,
): void {
unitsFast?: string,
): { units: string | null; length: string | null } | null {
const B = host.B;
const next = (cond: string): void => {
if (cond === "true") return;
@@ -999,19 +1134,36 @@ export function emitBytesLoopVersionGuard(
}
return value;
});
const lengths = receivers.map((receiver, index) => {
const len = version.receivers[index]!.string
? emitStringByteLength(host, receiver)
: emitBytesHeaderLength(host, receiver);
const unitsIndex = unitsFast !== undefined ? version.unitsReceiver : null;
const units: { read: { pointer: string; taken: string; published: string } | null } = {
read: null,
};
const widen = (len: string): string => {
if (host.sizeType === "i64") return len;
const wide = B.tmp();
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
return wide;
};
const lengths = receivers.map((receiver, index) => {
if (index === unitsIndex) {
const read = emitUnitsLength(host, receiver);
units.read = read;
return read.length;
}
return widen(
version.receivers[index]!.string
? emitStringByteLength(host, receiver)
: emitBytesHeaderLength(host, receiver),
);
});
const conditions: string[] = [];
// The published count is the receiver's UTF-16 length only while the
// runtime publishes the receiver itself.
if (units.read) conditions.push(units.read.published);
// A string's byte length is its UTF-16 length when it is ASCII.
version.receivers.forEach(({ string }, index) => {
if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!));
if (string && index !== unitsIndex)
conditions.push(emitStringAsciiFlag(host, receivers[index]!));
});
for (const index of version.ownedReceivers) {
const owned = B.tmp();
@@ -1023,6 +1175,31 @@ export function emitBytesLoopVersionGuard(
B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`);
conditions.push(ok);
}
if (version.scopes) {
// Every pair of scoped views covers disjoint bytes: one ends at or
// before the other starts. An empty view covers none.
const spans = version.scopes.receivers.map((index) => {
const type = version.receivers[index]!.ref.type as IrType & { kind: "bytes" };
const start = B.tmp(),
size = B.tmp(),
end = B.tmp();
B.line(`${start} = ptrtoint ptr ${emitBytesData(host, receivers[index]!)} to i64`);
B.line(`${size} = mul nuw i64 ${lengths[index]}, ${BYTES_ELEMENT_SIZE[type.elem]}`);
B.line(`${end} = add nuw i64 ${start}, ${size}`);
return { start, end };
});
spans.forEach((a, i) => {
for (const b of spans.slice(i + 1)) {
const before = B.tmp(),
after = B.tmp(),
apart = B.tmp();
B.line(`${before} = icmp ule i64 ${a.end}, ${b.start}`);
B.line(`${after} = icmp ule i64 ${b.end}, ${a.start}`);
B.line(`${apart} = or i1 ${before}, ${after}`);
conditions.push(apart);
}
});
}
for (const counter of version.counters) {
const value = host.emitExpr(counter.ref);
// A counter held as an integer (an int32 slot) is exact already and
@@ -1114,7 +1291,7 @@ export function emitBytesLoopVersionGuard(
// Unreachable: every term has a proven range. Stay checked.
B.br(slow);
B.startBlock(B.newLabel("bytes.version.dead"));
return;
return null;
}
const wide = widenInteger(host, integer);
const sum = B.tmp();
@@ -1143,6 +1320,25 @@ export function emitBytesLoopVersionGuard(
conditions.push(both);
}
}
if (version.paired !== null) {
// Both counters stay within [lo, hi] (exact integers below 2^53): every
// access at either one is in bounds when `lo >= 0` and `hi < length`.
const { loop } = version.paired;
const [low, high] = [loop.low.start, loop.high.start].map((expr) => {
const tested = emitStartIntegerTest(host, expr, -SAFE, SAFE);
if (tested.ok !== "true") conditions.push(tested.ok);
return tested.integer;
});
const nonNegative = B.tmp();
B.line(`${nonNegative} = icmp sge i64 ${low}, 0`);
conditions.push(nonNegative);
for (const receiver of version.paired.receivers) {
// Lengths are at most 2^53 elements: a signed comparison is exact.
const below = B.tmp();
B.line(`${below} = icmp slt i64 ${high}, ${lengths[receiver]}`);
conditions.push(below);
}
}
let valid = "true";
for (const cond of conditions) {
if (valid === "true") valid = cond;
@@ -1152,41 +1348,83 @@ export function emitBytesLoopVersionGuard(
valid = all;
}
}
if (valid === "true") B.br(fast);
else B.condBr(valid, fast, slow);
const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null;
const taken = units.read;
const target = taken ? B.newLabel("bytes.version.units") : fast;
if (valid === "true") B.br(target);
else B.condBr(valid, target, slow);
if (!taken) return { units: null, length };
B.startBlock(target);
B.condBr(taken.taken, unitsFast!, fast);
return { units: taken.pointer, length };
}
/** Tests that a double is an exact integer within [min, max] (not -0; NaN
* fails the ordered comparisons) and gives its i64. The integer is frozen,
* so it is a fixed value even when the test fails. */
function emitExactIntegerTest(
/** The length of a version's units receiver `s` in UTF-16 units: its byte
* length when it is ASCII; otherwise, once it is the scan receiver, the
* published unit count (0 when the runtime does not hold its units). When
* `s` is not already the scan receiver, the runtime converts it to units
* and publishes it (scr_str_scan_units). Also the units pointer, whether
* the loop is to read it, and whether the published receiver is `s`. */
function emitUnitsLength(
host: LlvmEmitterContext,
value: string,
min: number,
max: number,
): { ok: string; integer: string } {
s: string,
): { length: string; pointer: string; taken: string; published: string } {
const B = host.B;
const low = B.tmp(),
high = B.tmp(),
inRange = B.tmp(),
raw = B.tmp(),
integer = B.tmp(),
back = B.tmp(),
bits = B.tmp(),
backBits = B.tmp(),
exact = B.tmp(),
ok = B.tmp();
B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`);
B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`);
B.line(`${inRange} = and i1 ${low}, ${high}`);
B.line(`${raw} = fptosi double ${value} to i64`);
B.line(`${integer} = freeze i64 ${raw}`);
B.line(`${back} = sitofp i64 ${integer} to double`);
B.line(`${bits} = bitcast double ${value} to i64`);
B.line(`${backBits} = bitcast double ${back} to i64`);
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
return { ok, integer };
const sz = host.sizeType;
const type = stringScanType(sz);
const ascii = emitStringAsciiFlag(host, s);
const bytes = emitStringByteLength(host, s);
const from = B.currentLabel();
const publish = B.newLabel("bytes.version.publish"),
join = B.newLabel("bytes.version.length");
B.condBr(ascii, join, publish);
B.startBlock(publish);
// When `s` is already the scan receiver (an earlier loop over it published
// it), its units stay published until the runtime clears the scan: skip
// the call.
const held = B.tmp(),
holds = B.tmp();
B.line(`${held} = load ptr, ptr @scr_str_scan`);
B.line(`${holds} = icmp eq ptr ${held}, ${s}`);
const call = B.newLabel("bytes.version.publish.units"),
load = B.newLabel("bytes.version.publish.load");
B.condBr(holds, load, call);
B.startBlock(call);
host.declare(STRING_SCAN_UNITS_DECL);
const receiver = B.tmp(),
same = B.tmp(),
pointerSlot = B.tmp(),
pointer = B.tmp(),
countSlot = B.tmp(),
count = B.tmp(),
published = B.tmp();
B.line(`call void @scr_str_scan_units(ptr ${s})`);
B.br(load);
B.startBlock(load);
B.line(`${receiver} = load ptr, ptr @scr_str_scan`);
B.line(`${same} = icmp eq ptr ${receiver}, ${s}`);
B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`);
B.line(`${pointer} = load ptr, ptr ${pointerSlot}`);
B.line(`${countSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 2`);
B.line(`${count} = load ${sz}, ptr ${countSlot}`);
B.line(`${published} = select i1 ${same}, ${sz} ${count}, ${sz} 0`);
const end = B.currentLabel();
B.br(join);
B.startBlock(join);
const length = B.tmp(),
units = B.tmp(),
taken = B.tmp();
B.line(`${length} = phi ${sz} [ ${bytes}, %${from} ], [ ${published}, %${end} ]`);
B.line(`${units} = phi ptr [ null, %${from} ], [ ${pointer}, %${end} ]`);
B.line(`${taken} = phi i1 [ false, %${from} ], [ true, %${end} ]`);
const exact = B.tmp();
B.line(`${exact} = phi i1 [ true, %${from} ], [ ${same}, %${end} ]`);
let wide = length;
if (sz !== "i64") {
wide = B.tmp();
B.line(`${wide} = zext ${sz} ${length} to i64`);
}
return { length: wide, pointer: units, taken, published: exact };
}
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
@@ -760,3 +760,154 @@ test("a parameter stored as its last use takes the owned convention", () => {
const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]);
expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
});
test("a wrap into a pointer union forwards the wrapped binding to the callee's parameter", () => {
const wrap = (id: string): IrExpr => ({
kind: "unionWrap",
unionId: "optional",
tag: 0,
value: ref(id, record),
type: optional,
loc,
});
const leaf = helper("leaf");
const outer = helper("outer");
outer.params[0]!.type = outer.locals[0]!.type = record;
outer.locals.push(local("cell", record));
outer.body = [
{
kind: "varDecl",
localId: "cell",
init: { kind: "recordLit", fields: [], type: record, loc },
loc,
},
{ kind: "exprStmt", expr: call("leaf", [wrap("value")]), loc },
ret(call("leaf", [wrap("cell")])),
];
const functions = new Map([leaf, outer].map((fn) => [fn.name, fn]));
const analyzeWith = (pointer: boolean) =>
analyzeCallLifetimes(
functions,
undefined,
undefined,
undefined,
undefined,
undefined,
() => pointer,
);
// A boxed union builds a new box around the payload: a whole-value use.
expect(analyzeWith(false).parameters.get("outer")).toBeUndefined();
expect(analyzeWith(false).locals.get("outer")).toEqual(new Set());
// A pointer union passes the payload pointer itself.
expect(analyzeWith(true).parameters.get("outer")).toEqual(new Set([0]));
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set(["cell"]));
// A callee that keeps its parameter keeps the wrapped binding too.
leaf.body.push(ret(ref("value")));
expect(analyzeWith(true).parameters.get("outer")).toBeUndefined();
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set());
});
test("a pointer-wrapped binding needs a callee that keeps the narrowed payload in place", () => {
const wrapCell: IrExpr = {
kind: "unionWrap",
unionId: "optional",
tag: 0,
value: ref("cell", record),
type: optional,
loc,
};
const outer = helper("outer", []);
outer.locals.push(local("cell", record));
outer.body = [
{
kind: "varDecl",
localId: "cell",
init: { kind: "recordLit", fields: [], type: record, loc },
loc,
},
ret(call("leaf", [wrapCell])),
];
const read = (value: IrExpr): IrExpr => ({
kind: "recordGet",
obj: value,
shapeId: "cell",
field: "x",
type: F64,
loc,
});
const keeps = (leaf: IrFunction, ...more: IrFunction[]): boolean => {
const result = analyzeCallLifetimes(
new Map([outer, leaf, ...more].map((fn) => [fn.name, fn])),
(className) => className === "Node",
undefined,
undefined,
undefined,
undefined,
() => true,
);
return result.locals.get("outer")?.has("cell") !== true;
};
const leafWith = (body: IrStmt[]): IrFunction => ({ ...helper("leaf"), body });
// Tag tests and field reads of the narrowed payload leave it in place.
expect(keeps(leafWith([{ kind: "exprStmt", expr: tag("value"), loc }]))).toBe(false);
expect(keeps(leafWith([ret(read(narrow("value")))]))).toBe(false);
// So do field reads through a checked narrowing, which throws on the
// other arm (`value.x` of a `T | undefined` parameter).
const checked: IrExpr = {
kind: "ternary",
cond: tag("value"),
then: { kind: "libCall", fn: "error.nodeThrow", args: [], type: record, loc },
else_: narrow("value"),
type: record,
loc,
};
expect(keeps(leafWith([ret(read(checked))]))).toBe(false);
// Returning it keeps the payload.
const returnsChecked = leafWith([ret(checked)]);
returnsChecked.returnType = record;
expect(keeps(returnsChecked)).toBe(true);
// Returning the narrowed payload, or a narrowed nullish read, keeps it,
// although the union itself is only projected.
const returned = leafWith([ret(narrow("value"))]);
returned.returnType = record;
expect(analyze(returned).parameters.get("leaf")).toEqual(new Set([0]));
expect(keeps(returned)).toBe(true);
const defaulted = leafWith([
ret({ kind: "nullish", left: ref("value"), right: ref("other", record), type: record, loc }),
]);
defaulted.locals.push(local("other", record));
expect(keeps(defaulted)).toBe(true);
// So does a nullable-pointer field store of the union.
const self: IrType = { kind: "object", className: "Node" };
const stored = leafWith([
{
kind: "fieldSet",
obj: ref("this", self),
className: "Node",
field: "next",
value: ref("value"),
loc,
},
]);
stored.params = [{ localId: "value", name: "value", type: optional }];
stored.locals.push(local("this", self));
expect(keeps(stored)).toBe(true);
// A narrowed payload passed on, or the union passed on, is in place when
// the next callee keeps it in place.
const inner = (body: IrStmt[], type: IrType = record): IrFunction => {
const fn = helper("inner");
fn.params[0]!.type = fn.locals[0]!.type = type;
fn.body = body;
return fn;
};
const passNarrowed = leafWith([ret(call("inner", [narrow("value")]))]);
expect(keeps(passNarrowed, inner([ret(read(ref("value", record)))]))).toBe(false);
const returnsRecord = inner([ret(ref("value", record))]);
returnsRecord.returnType = record;
expect(keeps(passNarrowed, returnsRecord)).toBe(true);
const passUnion = leafWith([ret(call("inner", [ref("value")]))]);
expect(keeps(passUnion, inner([ret(read(narrow("value")))], optional))).toBe(false);
const innerReturned = inner([ret(narrow("value"))], optional);
innerReturned.returnType = record;
expect(keeps(passUnion, innerReturned)).toBe(true);
});
@@ -14,6 +14,11 @@ import { storedLastUseRead } from "./local-extents.js";
interface ForwardedUse {
callee: string;
index: number;
/** The binding reached the call inside a nullable-pointer union
* (PointerWrapTest): the callee's parameter holds the binding itself as
* its payload, so the callee must keep the payload, not only the union,
* in place. */
wrapped: boolean;
}
interface Uses {
@@ -21,6 +26,13 @@ interface Uses {
written: Set<string>;
declarations: Map<string, number>;
forwards: Map<string, ForwardedUse[]>;
/** Union bindings whose payload a use may keep: a narrowed read that is
* not projected or passed on, a narrowed nullish read, a store into a
* nullable-pointer field. The union itself can still be only projected. */
payloadKept: Set<string>;
/** Narrowed payloads of union bindings passed directly as call arguments:
* the payload stays in place if the callee's parameter is proved. */
payloadForwards: Map<string, ForwardedUse[]>;
}
interface Parameter {
@@ -99,19 +111,55 @@ const NO_INLINE_FIELDS: InlineFieldTest = { record: () => false, classField: ()
* reads the record and keeps no reference to it. */
export type RecordDynCopyTest = (shapeId: string) => boolean;
/** Wraps into a nullable-pointer union whose value is the payload pointer
* itself (LlEmitter.borrowableNullableWrap): the wrapped binding reaches the
* callee's parameter unchanged. */
export type PointerWrapTest = (wrap: IrExpr & { kind: "unionWrap" }) => boolean;
function collectUses(
fn: IrFunction,
nullableField: NullableFieldTest,
registerReturn: boolean,
inlineField: InlineFieldTest,
recordDynCopy: RecordDynCopyTest,
pointerWrap: PointerWrapTest,
): Uses {
const uses: Uses = {
invalid: new Set(),
written: new Set(),
declarations: new Map(),
forwards: new Map(),
payloadKept: new Set(),
payloadForwards: new Map(),
};
const forward = (
table: Map<string, ForwardedUse[]>,
localId: string,
use: ForwardedUse,
): void => {
let forwards = table.get(localId);
if (!forwards) table.set(localId, (forwards = []));
forwards.push(use);
};
/** The union binding `x` whose payload `value` is: `unionNarrow(x)`, or a
* checked narrowing `unionIsTag(x) ? <libCall> : unionNarrow(x)` (either
* order), whose other branch is visited as any use. */
function narrowedBinding(value: IrExpr): string | null {
if (value.kind === "unionNarrow")
return value.value.kind === "varRef" ? value.value.localId : null;
if (
value.kind !== "ternary" ||
value.cond.kind !== "unionIsTag" ||
value.cond.value.kind !== "varRef"
)
return null;
const id = value.cond.value.localId;
if (value.else_.kind === "libCall" && narrowedBinding(value.then) === id)
return expr(value.else_) ? id : null;
if (value.then.kind === "libCall" && narrowedBinding(value.else_) === id)
return expr(value.then) ? id : null;
return null;
}
const aliases: { alias: string; source: string }[] = [];
const stringInput = (value: IrExpr): boolean => {
if (value.kind === "varRef" && value.type.kind === "string") return true;
@@ -145,6 +193,12 @@ function collectUses(
if (borrowsMapReadInputs(node)) return expr(node.receiver) && node.args.every(stringInput);
break;
case "unionNarrow":
// The payload may escape; the union itself does not.
if (node.value.kind === "varRef") {
uses.payloadKept.add(node.value.localId);
return true;
}
break;
case "unionIsTag":
if (node.value.kind === "varRef") return true;
break;
@@ -161,12 +215,16 @@ function collectUses(
node.left.kind === "varRef" &&
node.left.type.kind === "union" &&
node.type.kind !== "union"
)
) {
uses.payloadKept.add(node.left.localId);
return expr(node.right);
}
break;
case "fieldGet":
case "recordGet":
if (node.obj.kind === "varRef") return true;
// A field read of a binding, or of a union binding's narrowed
// payload, keeps neither.
if (node.obj.kind === "varRef" || narrowedBinding(node.obj) !== null) return true;
break;
case "dynFrom":
// A copied record converts field by field; a live capsule keeps it.
@@ -200,13 +258,19 @@ function collectUses(
return node.args.every(expr);
case "call":
node.args.forEach((arg, index) => {
if (arg.kind !== "varRef") {
const narrowed = narrowedBinding(arg);
if (narrowed !== null) {
forward(uses.payloadForwards, narrowed, { callee: node.callee, index, wrapped: false });
return;
}
const wrapped =
arg.kind === "unionWrap" && arg.value.kind === "varRef" && pointerWrap(arg);
const value = wrapped ? arg.value : arg;
if (value.kind !== "varRef") {
expr(arg);
return;
}
let forwards = uses.forwards.get(arg.localId);
if (!forwards) uses.forwards.set(arg.localId, (forwards = []));
forwards.push({ callee: node.callee, index });
forward(uses.forwards, value.localId, { callee: node.callee, index, wrapped });
});
return true;
case "assignExpr":
@@ -250,8 +314,11 @@ function collectUses(
node.value.kind === "varRef" &&
node.value.type.kind === "union" &&
nullableField(node.className, node.field)
)
) {
// The field retains the payload.
uses.payloadKept.add(node.value.localId);
return expr(node.obj);
}
const stored = fieldStore(
node.obj,
node.value,
@@ -345,10 +412,14 @@ export function analyzeCallLifetimes(
registerReturns: ReadonlySet<string> = new Set(),
inlineField: InlineFieldTest = NO_INLINE_FIELDS,
recordDynCopy: RecordDynCopyTest = () => false,
pointerWrap: PointerWrapTest = () => false,
): CallLifetimes {
const usesByFunction = new Map<string, Uses>();
const lazyCaptures = new LazyCaptures(functions);
const nodes = new Map<string, Parameter[]>();
/** Union parameters that keep their payload in place too, which a
* pointer-wrapped argument needs (ForwardedUse.wrapped). */
const payloadNodes = new Map<string, (Parameter | undefined)[]>();
const unsafe: Parameter[] = [];
const consumed: { fn: string; index: number }[] = [];
const result: CallLifetimes = {
@@ -361,7 +432,7 @@ export function analyzeCallLifetimes(
};
for (const fn of functions.values()) {
if (fn.generator !== undefined && fn.classCaptures === undefined) {
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy);
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy, pointerWrap);
const locals = new Map(fn.locals.map((local) => [local.id, local]));
const stable = new Set<string>();
for (const param of fn.params) {
@@ -386,6 +457,7 @@ export function analyzeCallLifetimes(
registerReturns.has(fn.name),
inlineField,
recordDynCopy,
pointerWrap,
);
usesByFunction.set(fn.name, uses);
// Class-capturing bodies get only local facts (computed below from
@@ -460,23 +532,50 @@ export function analyzeCallLifetimes(
return node;
}),
);
payloadNodes.set(
fn.name,
fn.params.map((param) => {
if (param.type.kind !== "union") return undefined;
const node: Parameter = { safe: !uses.payloadKept.has(param.localId), callers: new Set() };
if (!node.safe) unsafe.push(node);
return node;
}),
);
}
/** The fact a forwarded use needs from the callee's parameter. */
const target = (use: ForwardedUse): Parameter | undefined =>
use.wrapped ? payloadNodes.get(use.callee)?.[use.index] : nodes.get(use.callee)?.[use.index];
for (const fn of functions.values()) {
const params = nodes.get(fn.name);
const uses = usesByFunction.get(fn.name);
if (!params || !uses) continue;
fn.params.forEach((param, index) => {
const node = params[index]!;
const depend = (node: Parameter, callees: (Parameter | undefined)[]): void => {
if (!node.safe) return;
for (const use of uses.forwards.get(param.localId) ?? []) {
const callee = nodes.get(use.callee)?.[use.index];
for (const callee of callees) {
if (!callee) {
node.safe = false;
unsafe.push(node);
break;
return;
}
callee.callers.add(node);
}
};
fn.params.forEach((param, index) => {
const node = params[index]!;
const forwards = uses.forwards.get(param.localId) ?? [];
depend(node, forwards.map(target));
const payload = payloadNodes.get(fn.name)?.[index];
if (!payload) return;
// The payload stays in place when the union does, when every callee
// the union reaches keeps its payload too, and when every callee its
// narrowed payload reaches proves that parameter.
depend(payload, [
node,
...forwards.map((use) => payloadNodes.get(use.callee)?.[use.index]),
...(uses.payloadForwards.get(param.localId) ?? []).map(
(use) => nodes.get(use.callee)?.[use.index],
),
]);
});
}
for (let i = 0; i < unsafe.length; i++) {
@@ -508,7 +607,7 @@ export function analyzeCallLifetimes(
)
continue;
const forwards = uses.forwards.get(local.id) ?? [];
if (forwards.every((use) => result.parameters.get(use.callee)?.has(use.index) === true)) {
if (forwards.every((use) => target(use)?.safe === true)) {
projected.add(local.id);
if (eligible(local) && !uses.written.has(local.id)) safe.add(local.id);
}
@@ -5,6 +5,8 @@
export function preservesDynTest(test: string): boolean {
switch (test) {
case "truthy":
case "classCapsule":
case "funcKind":
case "nullish":
case "promise":
case "buffer":

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