mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Merge remote-tracking branch 'origin/ni/3-hosted-loop' into HEAD
# Conflicts: # packages/cli/src/bootstrap.ts # packages/compiler/src/diagnostics/diagnostic.ts # packages/compiler/src/node-islands/libnode.ts
This commit is contained in:
@@ -52,9 +52,13 @@ jobs:
|
||||
const stackBase = pr?.stack?.base?.ref;
|
||||
const prBase = pr?.base?.ref;
|
||||
// Position 1 can already be merged; compare the current base branches.
|
||||
// A PR into the default branch is the lowest unmerged layer of any
|
||||
// stack, whatever the stack metadata names as its base.
|
||||
const defaultBranch = event.repository?.default_branch;
|
||||
const upperLayer = process.env.GITHUB_EVENT_NAME === 'pull_request' &&
|
||||
sameRepository && trustedAuthor && typeof stackBase === 'string' && stackBase.length > 0 &&
|
||||
typeof prBase === 'string' && prBase.length > 0 && stackBase !== prBase;
|
||||
typeof prBase === 'string' && prBase.length > 0 && stackBase !== prBase &&
|
||||
prBase !== defaultBranch;
|
||||
const full = !upperLayer;
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `full=${full}\n`);
|
||||
console.log(full ? 'Run full CI' : 'Defer full CI until this PR becomes the lowest unmerged layer');
|
||||
@@ -122,6 +126,8 @@ jobs:
|
||||
SCRIPTC_TEST_WORKERS: "1"
|
||||
- name: Verify mixed-source generated AST getters
|
||||
run: pnpm generate:ts7-ast --check
|
||||
- name: Verify the generated IR field readers
|
||||
run: pnpm generate:ir-fields --check
|
||||
|
||||
native_macos:
|
||||
needs: [validation_scope, lanes]
|
||||
@@ -220,9 +226,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:
|
||||
@@ -335,6 +356,37 @@ jobs:
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
|
||||
- run: pnpm test tests/harness/effect.test.ts tests/harness/effect4.test.ts
|
||||
|
||||
# The sanitized three.js nodes build alone: beside the san corpus's other
|
||||
# workers it took 860 to more than 1200 seconds of its shard
|
||||
# (SCRIPTC_THREE_SANITIZED_NODES_JOB in scripts/ci-test-plan.mjs). It keeps
|
||||
# the corpus's LeakSanitizer settings.
|
||||
three_nodes_san:
|
||||
needs: [validation_scope, lanes, native_linux]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (Linux, san, three.js nodes)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
SCRIPTC_SAN: "1"
|
||||
ASAN_OPTIONS: detect_leaks=1
|
||||
LSAN_OPTIONS: suppressions=${{ github.workspace }}/tests/harness/lsan.supp:print_suppressions=0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: native-linux-x64-gnu
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
|
||||
- run: pnpm test tests/harness/three.test.ts -t "'nodes' runs statically through LLVM on 'native'"
|
||||
|
||||
# Build each instrumented seed once. Downstream contracts retain the same
|
||||
# compiler and source artifacts without repeating seed construction.
|
||||
bootstrap_seed:
|
||||
@@ -528,7 +580,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
|
||||
@@ -622,8 +676,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:
|
||||
@@ -936,6 +991,7 @@ jobs:
|
||||
tests_macos,
|
||||
native_linux,
|
||||
effects,
|
||||
three_nodes_san,
|
||||
bootstrap_seed,
|
||||
bootstrap,
|
||||
linux_host_clang,
|
||||
@@ -968,6 +1024,7 @@ jobs:
|
||||
lane tests_macos "${{ needs.tests_macos.result }}" "${{ needs.lanes.outputs.macos }}"
|
||||
lane native_linux "${{ needs.native_linux.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane effects "${{ needs.effects.result }}" "${{ needs.lanes.outputs.effects }}"
|
||||
lane three_nodes_san "${{ needs.three_nodes_san.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane bootstrap_seed "${{ needs.bootstrap_seed.result }}" "${{ needs.lanes.outputs.bootstrap }}"
|
||||
lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}"
|
||||
lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
name: LLVM distribution
|
||||
|
||||
# Builds the LLVM that the Linux release helpers link
|
||||
# (native/llvm-codegen/llvm-dist.mjs): LLVM from its release sources with
|
||||
# ThinLTO and native/llvm-codegen/llvm.profdata.gz, for each Linux
|
||||
# architecture. Each job builds a helper against it and one against LLVM's
|
||||
# release archive, requires the two to give identical objects on this
|
||||
# checkout's helper inputs (scripts/check-llvm-dist.mjs), prints both
|
||||
# helpers' times, and attaches the archive to the LLVM_DIST_TAG release.
|
||||
# The arm64 build uses the profile collected on x64; its job log reports how
|
||||
# many functions the profile no longer matches. Run it after a profile refresh
|
||||
# or an LLVM version change, with a new LLVM_DIST_TAG.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency: ${{ github.workflow }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build LLVM (${{ matrix.arch }})
|
||||
if: github.repository == 'vercel-labs/scriptc'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
runner: ubuntu-24.04
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# About an hour for LLVM on 4 cores, and 5 minutes for each helper link.
|
||||
timeout-minutes: 240
|
||||
permissions:
|
||||
contents: write
|
||||
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
|
||||
- name: Install LLVM's release archive and sources
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes ninja-build zlib1g-dev libzstd-dev
|
||||
base=https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8
|
||||
curl -fL --retry 3 -o "$RUNNER_TEMP/llvm.tar.xz" "$base/${{ matrix.llvm_asset }}.tar.xz"
|
||||
tar -xJf "$RUNNER_TEMP/llvm.tar.xz" -C "$RUNNER_TEMP"
|
||||
mv "$RUNNER_TEMP/${{ matrix.llvm_asset }}" "$RUNNER_TEMP/llvm-22.1.8"
|
||||
curl -fL --retry 3 -o "$RUNNER_TEMP/llvm-src.tar.xz" "$base/llvm-project-22.1.8.src.tar.xz"
|
||||
tar -xJf "$RUNNER_TEMP/llvm-src.tar.xz" -C "$RUNNER_TEMP"
|
||||
rm "$RUNNER_TEMP/llvm.tar.xz" "$RUNNER_TEMP/llvm-src.tar.xz"
|
||||
echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter @scriptc/compiler --filter scriptc build
|
||||
- name: Build the distribution
|
||||
run: >-
|
||||
node scripts/build-llvm-dist.mjs --source "$RUNNER_TEMP/llvm-project-22.1.8.src"
|
||||
--prefix "$RUNNER_TEMP/dist/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}"
|
||||
--archive "$RUNNER_TEMP/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}.tar.xz"
|
||||
--work "$RUNNER_TEMP/llvm-dist-work"
|
||||
- name: Build a helper against each LLVM
|
||||
env:
|
||||
HELPER: llvm-linux-${{ matrix.arch }}-gnu
|
||||
run: |
|
||||
LLVM_DIR="$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" pnpm --filter "@scriptc/$HELPER" build:native
|
||||
cp "packages/$HELPER/bin/scriptc-llvm-codegen" "$RUNNER_TEMP/helper-release-archive"
|
||||
LLVM_DIR="$RUNNER_TEMP/dist/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}/lib/cmake/llvm" \
|
||||
SCRIPTC_LLVM_THINLTO=1 pnpm --filter "@scriptc/$HELPER" build:native
|
||||
cp "packages/$HELPER/bin/scriptc-llvm-codegen" "$RUNNER_TEMP/helper-profiled"
|
||||
- name: Compare the helpers
|
||||
run: >-
|
||||
node scripts/check-llvm-dist.mjs --baseline "$RUNNER_TEMP/helper-release-archive"
|
||||
--candidate "$RUNNER_TEMP/helper-profiled" --rounds 3 --work "$RUNNER_TEMP/llvm-check"
|
||||
- name: Attach the archive to the release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARCHIVE: ${{ runner.temp }}/scriptc-llvm-22.1.8-linux-${{ matrix.arch }}.tar.xz
|
||||
run: |
|
||||
tag=$(node --input-type=module -e "import { LLVM_DIST_TAG } from './native/llvm-codegen/llvm-dist.mjs'; console.log(LLVM_DIST_TAG)")
|
||||
# Both architectures' jobs create the release if it is missing.
|
||||
gh release view "$tag" > /dev/null 2>&1 ||
|
||||
gh release create "$tag" --target "$GITHUB_SHA" --title "$tag" --latest=false \
|
||||
--notes "LLVM 22.1.8 built with ThinLTO and the helper profile, for the Linux LLVM helpers (scripts/build-llvm-dist.mjs)." ||
|
||||
gh release view "$tag" > /dev/null
|
||||
gh release upload "$tag" "$ARCHIVE" "$ARCHIVE.sha256" --clobber
|
||||
@@ -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"
|
||||
+211
-12
@@ -83,6 +83,7 @@ jobs:
|
||||
spawnSync('git', ['diff', '--quiet', run.head_sha, 'HEAD', '--', '.',
|
||||
':(exclude).github/workflows/release.yml', ':(exclude)scripts/verify-native-cli.mjs',
|
||||
':(exclude)scripts/package-native-cli.mjs', ':(exclude)tests/harness/native-cli-packaging.test.ts',
|
||||
':(exclude)scripts/wait-npm-available.mjs', ':(exclude)tests/harness/npm-available.test.ts',
|
||||
]).status !== 0) continue;
|
||||
const artifacts = api(`repos/${repo}/actions/runs/${run.id}/artifacts?per_page=100`).artifacts
|
||||
.filter((artifact) => !artifact.expired && artifact.size_in_bytes > 0);
|
||||
@@ -100,10 +101,65 @@ 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 and function layout on this commit
|
||||
# (scripts/refresh-cli-profile.mjs, scripts/refresh-cli-layout.mjs), so every
|
||||
# platform's CLI build optimizes and lays out the code it compiles. The
|
||||
# committed ones fall behind main within a day or two. If this job fails,
|
||||
# the CLI builds use the committed profile and layout.
|
||||
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" --work "$RUNNER_TEMP/cli-profile-work"
|
||||
- name: Collect the function layout
|
||||
run: >-
|
||||
node scripts/refresh-cli-layout.mjs --jobs 4 --instrumented "$RUNNER_TEMP/cli-profile-work/cli"
|
||||
--output "$RUNNER_TEMP/cli-profile/cli.layout.gz"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-profile
|
||||
path: |
|
||||
${{ runner.temp }}/cli-profile/cli.profdata.gz
|
||||
${{ runner.temp }}/cli-profile/cli.layout.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:
|
||||
@@ -124,6 +180,7 @@ jobs:
|
||||
runtime: runtime-linux-x64-gnu
|
||||
cli: cli-linux-x64-gnu
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
llvm_dist: x64
|
||||
use_zig: true
|
||||
- platform: linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
@@ -131,6 +188,7 @@ jobs:
|
||||
runtime: runtime-linux-arm64-gnu
|
||||
cli: cli-linux-arm64-gnu
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
llvm_dist: arm64
|
||||
use_zig: true
|
||||
- platform: windows-x64
|
||||
runner: windows-2022
|
||||
@@ -146,6 +204,7 @@ jobs:
|
||||
seed_helper: llvm-linux-x64-gnu
|
||||
cli_target: x86_64-linux-musl
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
llvm_dist: x64
|
||||
use_zig: true
|
||||
- platform: linux-arm64-musl
|
||||
runner: ubuntu-24.04-arm
|
||||
@@ -155,12 +214,14 @@ jobs:
|
||||
seed_helper: llvm-linux-arm64-gnu
|
||||
cli_target: aarch64-linux-musl
|
||||
llvm_asset: LLVM-22.1.8-Linux-ARM64
|
||||
llvm_dist: arm64
|
||||
use_zig: true
|
||||
- platform: wasm32-wasi
|
||||
runner: ubuntu-24.04
|
||||
helper: llvm-linux-x64-gnu
|
||||
runtime: runtime-wasm32-wasi
|
||||
llvm_asset: LLVM-22.1.8-Linux-X64
|
||||
llvm_dist: x64
|
||||
use_zig: true
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# The Intel macOS build took 24 minutes for 0.3.2 and passed 30 for 0.3.3.
|
||||
@@ -191,6 +252,28 @@ jobs:
|
||||
mv "$RUNNER_TEMP/${{ matrix.llvm_asset }}" "$RUNNER_TEMP/llvm-22.1.8"
|
||||
echo "LLVM_DIR=$RUNNER_TEMP/llvm-22.1.8/lib/cmake/llvm" >> "$GITHUB_ENV"
|
||||
echo "$RUNNER_TEMP/llvm-22.1.8/bin" >> "$GITHUB_PATH"
|
||||
# The Linux helpers link LLVM built with ThinLTO and a profile of the
|
||||
# helper's code generation (native/llvm-codegen/llvm-dist.mjs), from a
|
||||
# release of this repository. Without that archive they link LLVM's
|
||||
# release archive, which gives the same objects more slowly. The
|
||||
# wasm32-wasi job also uploads the x64 GNU helper, so it selects the same.
|
||||
- name: Select the profiled LLVM distribution (Linux)
|
||||
if: matrix.llvm_dist != ''
|
||||
env:
|
||||
ARCH: ${{ matrix.llvm_dist }}
|
||||
run: |
|
||||
tag=$(node --input-type=module -e "import { LLVM_DIST_TAG } from './native/llvm-codegen/llvm-dist.mjs'; console.log(LLVM_DIST_TAG)")
|
||||
asset=$(node --input-type=module -e "import { llvmDistAsset } from './native/llvm-codegen/llvm-dist.mjs'; console.log(llvmDistAsset(process.env.ARCH))")
|
||||
base="https://github.com/$GITHUB_REPOSITORY/releases/download/$tag"
|
||||
if curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset" "$base/$asset" &&
|
||||
curl -fsSL --retry 3 -o "$RUNNER_TEMP/$asset.sha256" "$base/$asset.sha256" &&
|
||||
(cd "$RUNNER_TEMP" && sha256sum --check "$asset.sha256"); then
|
||||
tar -xJf "$RUNNER_TEMP/$asset" -C "$RUNNER_TEMP"
|
||||
echo "SCRIPTC_HELPER_LLVM_DIR=$RUNNER_TEMP/${asset%.tar.xz}/lib/cmake/llvm" >> "$GITHUB_ENV"
|
||||
echo "The helper links $asset from release $tag"
|
||||
else
|
||||
echo "::warning::$asset is not available from release $tag; the helper links LLVM's release archive"
|
||||
fi
|
||||
- uses: vercel-labs/setup-zig@v1
|
||||
if: matrix.use_zig == true
|
||||
with:
|
||||
@@ -229,7 +312,15 @@ jobs:
|
||||
- name: Build native packages (POSIX)
|
||||
if: matrix.platform != 'windows-x64'
|
||||
run: |
|
||||
pnpm --filter @scriptc/${{ matrix.helper }} build:native
|
||||
if [ -n "${SCRIPTC_HELPER_LLVM_DIR:-}" ]; then
|
||||
LLVM_DIR="$SCRIPTC_HELPER_LLVM_DIR" SCRIPTC_LLVM_THINLTO=1 pnpm --filter @scriptc/${{ matrix.helper }} build:native || {
|
||||
echo "::warning::the helper did not build against the profiled LLVM; it links LLVM's release archive"
|
||||
rm -rf node_modules/.cache/scriptc-${{ matrix.helper }}
|
||||
pnpm --filter @scriptc/${{ matrix.helper }} build:native
|
||||
}
|
||||
else
|
||||
pnpm --filter @scriptc/${{ matrix.helper }} build:native
|
||||
fi
|
||||
if [ "${{ matrix.use_zig }}" = true ]; then
|
||||
CC=zig AR=zig pnpm --filter @scriptc/${{ matrix.runtime }} build:native
|
||||
else
|
||||
@@ -243,6 +334,33 @@ 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 and SCRIPTC_NATIVE_LAYOUT
|
||||
# stay unset and build-native-cli.mts uses the committed profile and layout.
|
||||
- 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"
|
||||
layout="$(dirname "$COLLECTED")/cli.layout.gz"
|
||||
if [ -f "$layout" ]; then
|
||||
echo "SCRIPTC_NATIVE_LAYOUT=$layout" >> "$GITHUB_ENV"
|
||||
echo "The native CLI builds with the function layout collected on $GITHUB_SHA"
|
||||
fi
|
||||
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:
|
||||
@@ -317,12 +435,15 @@ jobs:
|
||||
(needs.check-release.outputs.reuse_artifacts == 'true' ||
|
||||
(needs.build-native-packages.result == 'success' && needs.build-mobile-runtime-packs.result == 'success'))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
# Two waits for npm availability of at most 25 minutes each.
|
||||
timeout-minutes: 70
|
||||
environment: Release
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
id-token: write
|
||||
outputs:
|
||||
uploads: ${{ steps.publish.outputs.uploads }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -342,7 +463,7 @@ jobs:
|
||||
VERSION: ${{ needs.check-release.outputs.version }}
|
||||
RECEIPT_RUN: ${{ needs.check-release.outputs.receipt_run }}
|
||||
run: |
|
||||
touch "$RUNNER_TEMP/npm-accepted.txt"
|
||||
touch "$RUNNER_TEMP/npm-accepted.txt" "$RUNNER_TEMP/npm-accepted-times.txt"
|
||||
# Include this run so re-running only failed jobs restores its receipts.
|
||||
for run in "$GITHUB_RUN_ID" "$RECEIPT_RUN"; do
|
||||
if [ -z "$run" ]; then continue; fi
|
||||
@@ -351,6 +472,7 @@ jobs:
|
||||
if [ "$count" -eq 0 ]; then continue; fi
|
||||
gh run download "$run" --repo "$GITHUB_REPOSITORY" --pattern "npm-accepted-$VERSION-*" --dir "$RUNNER_TEMP/npm-receipts/$run"
|
||||
find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted.txt"
|
||||
find "$RUNNER_TEMP/npm-receipts/$run" -name npm-accepted-times.txt -exec cat {} + >> "$RUNNER_TEMP/npm-accepted-times.txt"
|
||||
done
|
||||
|
||||
- name: Setup pnpm
|
||||
@@ -400,6 +522,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Publish to npm
|
||||
id: publish
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -419,10 +542,14 @@ jobs:
|
||||
|
||||
# Upload acceptance and public availability are separate states.
|
||||
# Save acceptance immediately so retries never depend on registry lag.
|
||||
# npm-release.txt lists every package of the release, and
|
||||
# npm-accepted-times.txt the time npm accepted each upload, for the
|
||||
# npm-available job.
|
||||
publish_dir() {
|
||||
dir="$1"
|
||||
packed="${2:-}"
|
||||
name=$(node -p "require('./$dir/package.json').name")
|
||||
echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-release.txt"
|
||||
if grep -Fxq "$name@$VERSION" "$RUNNER_TEMP/npm-accepted.txt"; then
|
||||
echo "$name@$VERSION upload already accepted, skipping"
|
||||
return 0
|
||||
@@ -439,6 +566,7 @@ jobs:
|
||||
publish_log="$RUNNER_TEMP/npm-publish.log"
|
||||
if npm publish "$packed" $PROVENANCE --access public 2>&1 | tee "$publish_log"; then
|
||||
echo "$name@$VERSION" >> "$RUNNER_TEMP/npm-accepted.txt"
|
||||
echo "$name@$VERSION $(date +%s)" >> "$RUNNER_TEMP/npm-accepted-times.txt"
|
||||
return 0
|
||||
else
|
||||
publish_status=${PIPESTATUS[0]}
|
||||
@@ -461,8 +589,8 @@ jobs:
|
||||
}
|
||||
|
||||
# pnpm pack resolves workspace dependencies; npm handles OIDC.
|
||||
# Publish dependencies first. Registry propagation after an accepted
|
||||
# upload must not block the remaining packages or the GitHub release.
|
||||
# Publish dependencies first. The npm-available job waits for the
|
||||
# packages that nothing in this release depends on.
|
||||
publish_dir packages/runtime
|
||||
publish_dir packages/runtime-darwin-arm64
|
||||
publish_dir packages/llvm-darwin-arm64
|
||||
@@ -482,32 +610,88 @@ jobs:
|
||||
publish_dir packages/runtime-wasm32-wasi
|
||||
publish_dir packages/runtime-win32-x64-msvc
|
||||
publish_dir packages/llvm-win32-x64-msvc
|
||||
# A native CLI package depends on its platform's LLVM helper and
|
||||
# runtime pack packages, and scriptc on the native CLI packages.
|
||||
# Each group publishes only once the packages it depends on
|
||||
# resolve, so an install never finds a package whose dependencies
|
||||
# npm is still processing. A rerun skips accepted uploads and waits
|
||||
# again.
|
||||
wait_available() {
|
||||
awk 'FILENAME == ARGV[1] { accepted[$1] = $2; next } { print $1, accepted[$1] }' \
|
||||
"$RUNNER_TEMP/npm-accepted-times.txt" "$RUNNER_TEMP/npm-release.txt" > "$RUNNER_TEMP/npm-wait.txt"
|
||||
node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-wait.txt" --interval 30 --timeout 1500
|
||||
}
|
||||
wait_available
|
||||
for pkg in packages/cli-*; do publish_dir "$pkg"; done
|
||||
publish_dir packages/compiler
|
||||
publish_dir packages/experimental
|
||||
publish_dir packages/node
|
||||
publish_dir packages/threads
|
||||
publish_dir packages/types
|
||||
wait_available
|
||||
publish_dir packages/cli
|
||||
echo "All package uploads accepted; npm registry propagation may continue after this job." >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Each package of the release with the time its upload was accepted,
|
||||
# when a run recorded it.
|
||||
{
|
||||
echo "uploads<<NPM_UPLOADS"
|
||||
awk 'FILENAME == ARGV[1] { accepted[$1] = $2; next } { print $1, accepted[$1] }' \
|
||||
"$RUNNER_TEMP/npm-accepted-times.txt" "$RUNNER_TEMP/npm-release.txt"
|
||||
echo "NPM_UPLOADS"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Save accepted upload receipts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-accepted-${{ needs.check-release.outputs.version }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/npm-accepted.txt
|
||||
path: |
|
||||
${{ runner.temp }}/npm-accepted.txt
|
||||
${{ runner.temp }}/npm-accepted-times.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 90
|
||||
|
||||
# Publish standalone native distributions and the surface manifest after
|
||||
# npm publishing succeeds. The release body comes from the marked changelog.
|
||||
github-release:
|
||||
name: Create GitHub Release
|
||||
# Waits until every package of the release resolves on the npm registry,
|
||||
# and reports each package's time from upload to available. Fails with the
|
||||
# packages that are still missing after 2 hours. It only reads from the
|
||||
# registry; the next release run retries it, and publishing skips uploads
|
||||
# npm already accepted.
|
||||
npm-available:
|
||||
name: Wait for npm availability
|
||||
needs: [check-release, publish]
|
||||
if: >-
|
||||
!cancelled() && needs.check-release.outputs.should_release == 'true' && needs.publish.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 135
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Wait for every package to resolve
|
||||
env:
|
||||
UPLOADS: ${{ needs.publish.outputs.uploads }}
|
||||
run: |
|
||||
printf '%s\n' "$UPLOADS" > "$RUNNER_TEMP/npm-uploads.txt"
|
||||
node scripts/wait-npm-available.mjs "$RUNNER_TEMP/npm-uploads.txt"
|
||||
|
||||
# Publish standalone native distributions and the surface manifest after
|
||||
# every npm package resolves. The release body comes from the marked
|
||||
# changelog.
|
||||
github-release:
|
||||
name: Create GitHub Release
|
||||
needs: [check-release, publish, npm-available]
|
||||
if: >-
|
||||
!cancelled() && needs.check-release.outputs.should_release == 'true' && needs.npm-available.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -559,6 +743,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 }}"
|
||||
|
||||
+13
-1
@@ -283,10 +283,22 @@
|
||||
},
|
||||
{
|
||||
// Ambient declarations describe an existing constructor and its prototype methods.
|
||||
"files": ["packages/compiler/ambient/scriptc-node-fallback.d.ts"],
|
||||
"files": [
|
||||
"packages/compiler/ambient/scriptc-node-fallback.d.ts",
|
||||
"packages/compiler/ambient/node-fallback/*.d.ts",
|
||||
],
|
||||
"rules": {
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Each module's fallback declarations are a script that declares the
|
||||
// module ambiently; it names the files of the modules it uses by
|
||||
// reference, since a script cannot import them.
|
||||
"files": ["packages/compiler/ambient/node-fallback/*.d.ts"],
|
||||
"rules": {
|
||||
"typescript/triple-slash-reference": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
|
||||
|
||||
Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.
|
||||
|
||||
Name a new corpus program after what it tests, without a number: lowercase words joined by hyphens, such as `tests/corpus/intersection-restated-members.ts`, or a directory such as `tests/corpus/<name>/main.ts` for a multi-file program. Parallel changes kept taking the same next number; with names, two changes collide only when they add the same name, which git reports as a conflict. Leave existing numbered programs as they are: never rename or renumber them, and name a new program even when it extends a numbered family.
|
||||
|
||||
Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/<program>.json`, not in `tests/harness/corpus-costs.json`; see the README there.
|
||||
|
||||
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
|
||||
|
||||
+79
-2
@@ -6,6 +6,85 @@ All notable changes to scriptc will be documented in this file.
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
## 0.4.5
|
||||
|
||||
### Highlights
|
||||
|
||||
- **Fixes:**
|
||||
- Release builds read a typed array's length without a header load that could read a freed view.
|
||||
- `net.Socket` `write()` returns Node's backpressure boolean and fires `'drain'`.
|
||||
- Super calls in JavaScript method values call the declared base method.
|
||||
- Calling a non-function through an asserted callee throws Node's TypeError.
|
||||
- **Faster programs:**
|
||||
- Exceptions propagate by unwinding from directly called functions on Linux, instead of status checks after every call.
|
||||
- Typed-array loops prove their bounds and keep their views disjoint in more cases, including views over shared memory in worker programs; integer loops stay in integers across nested counted loops, pure helper calls, powers of two and byte reads.
|
||||
- Lone array callbacks run as loops with their bodies inlined, and row sums run two rows per pass.
|
||||
- Number-key maps look keys up on their integer view and write updates through the entry the read found; small number-key maps use a direct index.
|
||||
- Strings: base64 decodes eight characters at a time; Buffers fill, swap byte order and decode `utf16le` a word at a time; readline splits input in one scan; string scans over non-ASCII strings use their UTF-16 units.
|
||||
- Untyped code: property caches also cache absent properties, object literal members go through the store caches, calls on plain arrays skip the method ladder, and dynamic values dispose without key-cache walks.
|
||||
- JSON parses into discriminated unions, and dynamic values convert into nested unions, in one pass.
|
||||
- BigInt division settles quotients from the top limbs, and limb shifts are vectorized.
|
||||
- **Faster builds:**
|
||||
- Function bodies are emitted on up to eight workers, including in dev builds, and repeated work in LLVM module emission is cut.
|
||||
- Linux executables link with lld when the driver finds it, and unchanged executables are reused without selecting the runtime pack.
|
||||
- Dev builds reuse their frontend output after edits that keep token positions, and unchanged frontend inputs are checked by file identity before hashing.
|
||||
- Lowering fetches only the syntax trees it reads, and the large compiler tables are built on first use.
|
||||
- Release builds of the native compiler use a profile collected on the release commit.
|
||||
- Type checking can use a tsc-ts server shared by later builds (opt-in).
|
||||
- **Node.js parity:**
|
||||
- Crypto: RSA key generation lengths, RSA-PSS keys, encrypted private keys, `X509Certificate` fields and methods, and TLS certificates and `tls.Server` contexts.
|
||||
- zlib: Brotli compression and decompression.
|
||||
- Dates are mutable objects with Node's setters and conversions; URL hosts and paths parse as the URL Standard does, including non-ASCII hosts with UTS #46.
|
||||
- `Math`, `JSON` and `Reflect` are real objects in JavaScript; objects can inherit from JavaScript class prototypes; typed arrays held as untyped values support named properties and `Symbol.species`.
|
||||
- Error messages: `JSON.parse` syntax errors use V8's messages and positions; `Object.getPrototypeOf` of an Error answers `Error.prototype`.
|
||||
- `util.inspect` of dynamic classes, functions and `%o` arguments, and `Symbol.toStringTag` fields and getters, match Node.
|
||||
- **npm packages:** more npm and CommonJS dependencies compile statically, including namespace-style packages with guarded declarations and `var` exports, and untyped Maps, Sets and arrays are typed from their uses.
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 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.
|
||||
|
||||
## 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 +238,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
|
||||
|
||||
+4
-2
@@ -14,9 +14,11 @@ To prepare a release:
|
||||
|
||||
CI (`.github/workflows/release.yml`) compares the version in `packages/cli/package.json` to what `scriptc` has on npm. If it differs, it builds platform packages on matching macOS, Linux, and Windows runners, verifies the version spine, and publishes the runtime, helper, compiler, and CLI packages in dependency order. After the publish succeeds, a separate job creates the git tag `v<version>` and the GitHub release with the marked changelog entry as its body, and attaches `surface-manifest.json` — the machine-readable listing of the surface the static tier compiles at that version (stable per-entry ids, so two releases diff mechanically; see `packages/compiler/src/coverage/surface-manifest.ts` for the schema). The job regenerates the manifest from the tree and fails on any byte difference from the committed file before attaching, so the asset is always the manifest of the code being released. The same file ships inside the `@scriptc/compiler` package as `@scriptc/compiler/surface-manifest.json`.
|
||||
|
||||
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.
|
||||
The release job builds each pinned LLVM helper, precompiled runtime pack, and native CLI on its matching host. The native CLI package bundles its TypeScript checker, declarations, and comptime evaluator, and depends on its platform's LLVM helper and runtime pack packages at the same version; the compiler resolves them beside itself. The standalone archives bundle the helper and runtime pack too. 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.
|
||||
|
||||
Builds of the native compiler also order its functions by the committed layout `packages/compiler/native/cli.layout.gz`: the functions that `--version` and rebuilds of unchanged programs run come first, then the ones other builds run, ordered by the share of the layout's training builds that run each, so a run maps fewer pages of the executable. Functions the layout does not list keep their usual order. Function names of closures and generated helpers change with nearby edits, so the `cli-profile` job also collects a layout on the release commit (`scripts/refresh-cli-layout.mjs`, reusing the job's instrumented compiler), and the CLI builds use it (`SCRIPTC_NATIVE_LAYOUT`). Refresh the committed layout the same way as the profile, with `node scripts/refresh-cli-layout.mjs`, and commit it 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
|
||||
|
||||
@@ -10,12 +10,18 @@ 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 |
|
||||
| `handler-tables` | cpu | dispatch tables of functions: `Record<string, fn>` read as `t[k]!(…)`, `t[k]?.(…)` and through a checked local, a `Map<string, fn>`, an array of handlers, and an object literal of methods; some handlers declare fewer parameters than the table type |
|
||||
| `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 |
|
||||
@@ -25,12 +31,16 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
|
||||
| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` |
|
||||
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
|
||||
| `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset |
|
||||
| `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 |
|
||||
| `string-iteration` | cpu | code point iteration over text that mixes ASCII, Latin-1 letters, CJK and emoji: a for-of word tokenizer, spread reversal, and `Array.from` over words |
|
||||
| `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"
|
||||
]
|
||||
}
|
||||
@@ -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/handler-tables.ts",
|
||||
"args": [
|
||||
"4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Dispatch tables of functions: a `Record<string, fn>` of opcode handlers
|
||||
// called as `ops[name]!(…)`, `ops[name]?.(…)` and through a checked local,
|
||||
// a `Map<string, fn>` of the same handlers, an array of handlers indexed by
|
||||
// opcode number, and an object literal of methods. Some handlers declare
|
||||
// fewer parameters than the table's function type.
|
||||
type Op = (acc: number, arg: number) => number;
|
||||
|
||||
const ops: Record<string, Op> = {
|
||||
add: (acc, arg) => acc + arg,
|
||||
sub: (acc, arg) => acc - arg,
|
||||
mul: (acc, arg) => (acc * arg) % 1000003,
|
||||
neg: (acc) => -acc,
|
||||
half: (acc) => Math.floor(acc / 2),
|
||||
mix: (acc, arg) => (acc ^ arg) & 0xffff,
|
||||
};
|
||||
|
||||
const byName = new Map<string, Op>(Object.entries(ops));
|
||||
const byCode: Op[] = Object.values(ops);
|
||||
const names = Object.keys(ops);
|
||||
|
||||
const methods = {
|
||||
add(acc: number, arg: number): number {
|
||||
return acc + arg;
|
||||
},
|
||||
sub(acc: number, arg: number): number {
|
||||
return acc - arg;
|
||||
},
|
||||
mul(acc: number, arg: number): number {
|
||||
return (acc * arg) % 1000003;
|
||||
},
|
||||
};
|
||||
|
||||
interface Insn {
|
||||
name: string;
|
||||
code: number;
|
||||
arg: number;
|
||||
}
|
||||
|
||||
function program(length: number): Insn[] {
|
||||
const out: Insn[] = [];
|
||||
let seed = 12345;
|
||||
for (let i = 0; i < length; i++) {
|
||||
seed = (seed * 1103515245 + 12345) % 2147483648;
|
||||
const code = seed % (names.length + 1);
|
||||
// One in seven instructions names an opcode no table holds.
|
||||
out.push({ name: code < names.length ? names[code]! : "nop", code, arg: (seed >> 8) % 97 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function runAsserted(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) if (insn.name !== "nop") acc = ops[insn.name]!(acc, insn.arg);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runOptional(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) acc = ops[insn.name]?.(acc, insn.arg) ?? acc + 1;
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runChecked(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
const handler = ops[insn.name];
|
||||
if (handler) acc = handler(acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runMap(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
const handler = byName.get(insn.name);
|
||||
if (handler !== undefined) acc = handler(acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runArray(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) if (insn.code < byCode.length) acc = byCode[insn.code]!(acc, insn.arg);
|
||||
return acc;
|
||||
}
|
||||
|
||||
function runMethods(insns: Insn[]): number {
|
||||
let acc = 1;
|
||||
for (const insn of insns) {
|
||||
if (insn.code === 0) acc = methods.add(acc, insn.arg);
|
||||
else if (insn.code === 1) acc = methods.sub(acc, insn.arg);
|
||||
else if (insn.code === 2) acc = methods["mul"](acc, insn.arg);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "4");
|
||||
const insns = program(200_000);
|
||||
const totals = [0, 0, 0, 0, 0, 0];
|
||||
for (let round = 0; round < rounds * 10; round++) {
|
||||
totals[0] = (totals[0]! + runAsserted(insns)) % 1e9;
|
||||
totals[1] = (totals[1]! + runOptional(insns)) % 1e9;
|
||||
totals[2] = (totals[2]! + runChecked(insns)) % 1e9;
|
||||
totals[3] = (totals[3]! + runMap(insns)) % 1e9;
|
||||
totals[4] = (totals[4]! + runArray(insns)) % 1e9;
|
||||
totals[5] = (totals[5]! + runMethods(insns)) % 1e9;
|
||||
}
|
||||
console.log(totals.join(" "));
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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/regex-replacers.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Regex replace and replaceAll with inline callbacks, written as they
|
||||
// usually are, without parameter annotations: template placeholders looked
|
||||
// up in a dictionary, snake_case to camelCase, date reordering, optional
|
||||
// version parts, capitalization and a string search with its offset.
|
||||
let seed = 7;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function pick<T>(items: readonly T[]): T {
|
||||
return items[Math.floor(random() * items.length)]!;
|
||||
}
|
||||
|
||||
const words = ["user", "order", "item", "cart", "price", "total", "status", "created", "updated", "id"];
|
||||
const names = ["Ada", "Grace", "Linus", "Barbara", "Ken", "Margaret"];
|
||||
const vars: Record<string, string> = {
|
||||
name: "world",
|
||||
city: "Lisbon",
|
||||
day: "Tuesday",
|
||||
plan: "pro",
|
||||
count: "12",
|
||||
};
|
||||
const keys = [...Object.keys(vars), "missing"];
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const count = Math.floor(20000 * scale);
|
||||
const templates: string[] = [];
|
||||
const identifiers: string[] = [];
|
||||
const dates: string[] = [];
|
||||
const versions: string[] = [];
|
||||
const sentences: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
templates.push(`Hello {${pick(keys)}}, your {${pick(keys)}} plan renews on {${pick(keys)}} in {${pick(keys)}}.`);
|
||||
identifiers.push(`${pick(words)}_${pick(words)}_${pick(words)}${random() < 0.5 ? "_" + pick(words) : ""}`);
|
||||
const year = 1990 + Math.floor(random() * 40);
|
||||
const month = String(1 + Math.floor(random() * 12)).padStart(2, "0");
|
||||
const day = String(1 + Math.floor(random() * 28)).padStart(2, "0");
|
||||
dates.push(`from ${year}-${month}-${day} to ${year + 1}-${month}-${day} (${pick(names)})`);
|
||||
versions.push(`v${i % 9}.${i % 13}${random() < 0.5 ? "." + (i % 7) : ""} and v${i % 5}.${i % 3}`);
|
||||
sentences.push(`${pick(names)} said the ${pick(words)} and the ${pick(words)} were ready for ${pick(names)}`);
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
let checksum = 0;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const filled = templates[i]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`);
|
||||
const camel = identifiers[i]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
||||
const reordered = dates[i]!.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, m, d) => `${d}/${m}/${y}`);
|
||||
const padded = versions[i]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`);
|
||||
const titled = sentences[i]!.replace(/\b[a-z]/g, (c) => c.toUpperCase());
|
||||
const marked = sentences[i]!.replaceAll("the", (m, at) => (at % 2 === 0 ? m.toUpperCase() : m));
|
||||
length += filled.length + camel.length + reordered.length + padded.length + titled.length + marked.length;
|
||||
checksum = (checksum * 31 + camel.charCodeAt(camel.length - 1) + padded.charCodeAt(padded.length - 1)) >>> 0;
|
||||
}
|
||||
}
|
||||
console.log(templates[0]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`));
|
||||
console.log(identifiers[1]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()));
|
||||
console.log(versions[2]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`));
|
||||
console.log("length", length, "checksum", checksum);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/string-iteration.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Code point iteration over mixed text: a word tokenizer over for-of, a
|
||||
// character histogram, reversal through spread, and Array.from over
|
||||
// words. The text mixes ASCII, Latin-1 letters, CJK and emoji.
|
||||
let seed = 11;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
const pieces = [
|
||||
"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog",
|
||||
"café", "naïve", "über", "señor", "straße", "déjà", "vu",
|
||||
"日本", "語", "テキスト", "中文", "한국어",
|
||||
"😀", "🚀", "👍", "🎉",
|
||||
];
|
||||
const punctuation = [" ", " ", " ", ", ", ". ", "! ", "\n"];
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < Math.floor(4000 * scale); i++) {
|
||||
let line = "";
|
||||
const words = 6 + Math.floor(random() * 10);
|
||||
for (let w = 0; w < words; w++) {
|
||||
line += pieces[Math.floor(random() * pieces.length)]!;
|
||||
line += punctuation[Math.floor(random() * punctuation.length)]!;
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
function isWordChar(c: string): boolean {
|
||||
const code = c.codePointAt(0)!;
|
||||
return (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || code >= 0xc0;
|
||||
}
|
||||
|
||||
let tokens = 0;
|
||||
let tokenUnits = 0;
|
||||
let reversedLength = 0;
|
||||
let fromLength = 0;
|
||||
const histogram = new Map<string, number>();
|
||||
for (let round = 0; round < 30; round++) {
|
||||
for (const line of lines) {
|
||||
let word = "";
|
||||
for (const c of line) {
|
||||
if (isWordChar(c)) {
|
||||
word += c;
|
||||
continue;
|
||||
}
|
||||
if (word !== "") {
|
||||
tokens++;
|
||||
tokenUnits += word.length;
|
||||
if (round === 0) histogram.set(word, (histogram.get(word) ?? 0) + 1);
|
||||
fromLength += Array.from(word).length;
|
||||
word = "";
|
||||
}
|
||||
}
|
||||
reversedLength += [...line].reverse().join("").length;
|
||||
}
|
||||
}
|
||||
const top = [...histogram].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 5);
|
||||
console.log(top.map(([word, count]) => `${word}:${count}`).join(" "));
|
||||
console.log("tokens", tokens, "units", tokenUnits, "reversed", reversedLength, "from", fromLength);
|
||||
@@ -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);
|
||||
+108
-2
@@ -2,7 +2,7 @@
|
||||
title: "CLI Reference"
|
||||
---
|
||||
|
||||
The `scriptc` command has five subcommands: `build`, `run`, `coverage`, `cache warm`, and `merge-profiles`. `scriptc --help` prints this summary and the options below, and `scriptc --version` prints the installed version.
|
||||
The `scriptc` command has six subcommands: `build`, `run`, `coverage`, `types`, `cache warm`, and `merge-profiles`. `scriptc --help` prints this summary and the options below, and `scriptc --version` prints the installed version.
|
||||
|
||||
```console
|
||||
$ scriptc --help
|
||||
@@ -21,6 +21,13 @@ Usage:
|
||||
exporting the profile symbols; a profile
|
||||
with a sidecar section also gets the
|
||||
contract sidecar JSON beside the archive
|
||||
scriptc types patch <package> <selector> <type> [--range <r>] [--dry-run]
|
||||
fix a wrong declaration of an installed
|
||||
package: shows what the selector
|
||||
matches and adds the type patch to
|
||||
package.json ("| undefined" widens)
|
||||
scriptc types list the project's type patches and what
|
||||
they match
|
||||
scriptc cache warm [runtime|tls…] verify and load precompiled runtime families
|
||||
for the current compiler/SDK/target
|
||||
scriptc merge-profiles -o <out.profdata> [--sparse] <profile…>
|
||||
@@ -88,6 +95,24 @@ $ scriptc run fib.ts
|
||||
|
||||
Analyzes the program without building it and reports what compiles and what blocks the rest. See [Coverage Reports](/docs/coverage).
|
||||
|
||||
## scriptc types
|
||||
|
||||
Writes and lists [type patches](/docs/dependencies#fix-a-packages-types), the project's fixes for wrong declarations of npm packages.
|
||||
|
||||
<dl>
|
||||
<dt><code>scriptc types patch <package> <selector> <type></code></dt>
|
||||
<dd>Finds the package installed for the working directory, prints every declaration the selector matches in its declaration files, its JSDoc and its <code>@types/</code> package with the type before and after, and adds the patch to the nearest <code>package.json</code>. The patch joins an entry whose range covers the installed version, or gets the range <code>^<installed version></code>. Nothing is written when the selector matches nothing. <code>--range <range></code> chooses the range, and <code>--dry-run</code> only prints.</dd>
|
||||
<dt><code>scriptc types list</code></dt>
|
||||
<dd>Prints each type patch in the nearest <code>package.json</code>, whether it applies to the installed version, and what it matches.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
$ scriptc types list
|
||||
commander@^15.0.0
|
||||
HelpContext.helpWidth: | undefined
|
||||
node_modules/commander/lib/command.js:2481 number -> number | undefined
|
||||
```
|
||||
|
||||
## scriptc cache warm
|
||||
|
||||
Verifies and loads the installed precompiled runtime objects and vendored libraries, and prints the cache directory. Pass `runtime`, `tls`, or both to select which to warm. Ordinary builds don't need this step. With `--sanitize`, it builds the instrumented runtime objects using a C toolchain.
|
||||
@@ -135,11 +160,17 @@ 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 <baseline|x86-64-v2|x86-64-v3|x86-64-v4|native></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>--cpu-dispatch</code></dt>
|
||||
<dd>On x86-64 targets, compile the program's code twice, for the baseline and for <code>x86-64-v3</code>, and choose the copy when the program starts. The executable runs on any x86-64 CPU and uses the <code>x86-64-v3</code> code on CPUs that support it. Not used with a <code>--target-cpu</code> level above <code>baseline</code>. Valid only for executable builds. See <a href="#runtime-cpu-dispatch">Runtime CPU dispatch</a>.</dd>
|
||||
|
||||
<dt><code>--profile-generate <pattern></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>
|
||||
|
||||
<dt><code>--profile-use <file.profdata></code></dt>
|
||||
<dd>Optimize the executable with a profile from <code>scriptc merge-profiles</code>: code is laid out, inlined, and split by how the profiled runs used it. Code the profile does not describe, or describes from an older version of the program, is optimized as without a profile, so a stale profile only loses part of its gain. A new or changed profile compiles every partition of the program again. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
|
||||
<dd>Optimize the executable with a profile from <code>scriptc merge-profiles</code>: code is laid out, inlined, and split by how the profiled runs used it. Code the profile does not describe, or describes from an older version of the program, is optimized as without a profile, so a stale profile only loses part of its gain. A new or changed profile compiles every partition of the program again. <code>scriptc build</code> prints how the profile matched the program's functions on stderr; see <a href="#profile-guided-optimization">Profile-guided optimization</a>. Valid only for executable builds with <code>--optimization=release</code> or <code>speed</code>.</dd>
|
||||
|
||||
<dt><code>--npm-static <pkg[,pkg…]|auto></code></dt>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing <code>...</code>, as in <code>fs-extra...</code>, also names every package that package depends on. <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
@@ -207,6 +238,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 +262,78 @@ $ 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.
|
||||
|
||||
`scriptc build --profile-use` prints one line on stderr with how the profile matched the program's functions:
|
||||
|
||||
```
|
||||
scriptc: profile app.profdata: 9170 functions annotated, 0 mismatched, 28636 without a record
|
||||
```
|
||||
|
||||
- **annotated:** functions optimized with the profile's counts.
|
||||
- **mismatched:** functions whose control flow changed since the profile was collected. They are optimized without counts.
|
||||
- **without a record:** functions the profile does not describe: new functions, and functions the profiled runs never reached.
|
||||
- When the build reuses cached partitions whose counts were not recorded, the line ends with the number of those partitions.
|
||||
|
||||
When more than 10% of the functions the profile describes (annotated and mismatched) are mismatched, the build also prints a warning:
|
||||
|
||||
```
|
||||
scriptc: warning: 3739 of the 8947 functions app.profdata describes (41.8%) changed since it was collected and are optimized without their counts; collect the profile again
|
||||
```
|
||||
|
||||
The build still succeeds. Collecting the profile again with the current program and scriptc version restores the counts.
|
||||
|
||||
## 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.
|
||||
|
||||
### Runtime CPU dispatch
|
||||
|
||||
`--cpu-dispatch` keeps the executable portable and still uses the higher level where the CPU has it. The program's functions are compiled for the baseline and for `x86-64-v3`. At startup, the executable checks the CPU and the operating system's register support, and then runs the `x86-64-v3` code on CPUs that support every feature of that level. Calls between the program's functions stay within the chosen copy.
|
||||
|
||||
```console
|
||||
$ scriptc build app.ts --optimization=speed --cpu-dispatch -o app
|
||||
```
|
||||
|
||||
The second copy makes the executable larger and the first build of the program slower, since LLVM compiles the program's code twice. Unchanged rebuilds are not affected. Both copies compute the same results: neither contracts floating-point operations into fused multiply-adds.
|
||||
|
||||
The `SCRIPTC_CPU_LEVEL` environment variable caps the level the executable uses, for example to test the baseline code on a newer machine. It takes a level from `0` to `4`, `baseline`, or a level name such as `x86-64-v3`; any other value selects the baseline code.
|
||||
|
||||
```console
|
||||
$ SCRIPTC_CPU_LEVEL=baseline ./app
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -141,6 +141,75 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval
|
||||
|
||||
A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports.
|
||||
|
||||
## Fix a package's types
|
||||
|
||||
scriptc trusts declared types: a package's `.d.ts` files, its `@types/` package, and the JSDoc types in its JavaScript. A value that contradicts its declared type throws a `TypeError` instead of flowing on, so a package whose declarations are wrong can stop a program that runs under Node.js. A common case is a member declared `string` or `number` that is sometimes `undefined`.
|
||||
|
||||
When the type comes from a package's declarations, the error names the declaration and the patch that would admit the value:
|
||||
|
||||
```text
|
||||
Uncaught TypeError: expected string at $, got undefined
|
||||
profilekit@3.1.0 declares Profile.email as string (index.d.ts:7)
|
||||
if that declaration is wrong, patch it: scriptc types patch profilekit "Profile.email" "| undefined"
|
||||
```
|
||||
|
||||
Fix such a declaration in your project with a type patch. `scriptc types patch` shows what a declaration path matches in the installed package and adds the patch to your `package.json`:
|
||||
|
||||
```console
|
||||
$ scriptc types patch commander "HelpContext.helpWidth" "| undefined"
|
||||
commander 15.0.0 (node_modules/commander)
|
||||
lib/command.js:2481 number -> number | undefined
|
||||
wrote package.json scriptc.typePatches["commander@^15.0.0"].declarations["HelpContext.helpWidth"]
|
||||
```
|
||||
|
||||
The patch it writes looks like this:
|
||||
|
||||
```json:package.json
|
||||
{
|
||||
"scriptc": {
|
||||
"typePatches": {
|
||||
"commander@^15.0.0": {
|
||||
"declarations": {
|
||||
"HelpContext.helpWidth": "| undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each key names a package and a semver range of the installed versions the patch describes. `declarations` maps a declaration to a type:
|
||||
|
||||
<dl>
|
||||
<dt>`"| undefined"`</dt>
|
||||
<dd>A type that starts with `|` widens the declared type, here to `number | undefined`.</dd>
|
||||
<dt>`"string | null"`</dt>
|
||||
<dd>Any other type replaces the declared type.</dd>
|
||||
</dl>
|
||||
|
||||
A declaration is named by its path: `Options.name` for a member of a type, `parse()` for a function's return type, `parse(text)` for a parameter, `list()[]` for an array's elements, and `Env[string]` for an index signature. `export=()` names the return type of the function the module exports, `default` its default export, and a file prefix such as `index.js:parse()` names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its `@types/` package. The patched type then holds everywhere, so your code has to handle the `undefined` that a widened declaration allows. An optional `"note"` string can link the upstream issue.
|
||||
|
||||
For larger fixes, replace a whole declaration file, or give a package without declarations its own:
|
||||
|
||||
```json:package.json
|
||||
{
|
||||
"scriptc": {
|
||||
"typePatches": {
|
||||
"some-client@^3.2.0": {
|
||||
"files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" }
|
||||
},
|
||||
"untyped-helper@1.x": {
|
||||
"types": "./type-patches/untyped-helper.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`files` maps a declaration file inside the package, or inside its `@types/` package written as `@types/<name>/<path>`, to a local `.d.ts` file. `types` supplies the package's declaration entry. Paths of local files are relative to the `package.json`, and a replaced file can import the package's other declaration files with the same relative paths as the file it replaces.
|
||||
|
||||
Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later.
|
||||
|
||||
## Provenance sources (experimental)
|
||||
|
||||
`--provenance-sources` compiles a package from the source at the commit its npm provenance attestation names, instead of from its published JavaScript. This lets packages written in TypeScript compile from their original TypeScript. Packages without a usable attestation compile from their installed files as usual.
|
||||
|
||||
@@ -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`:
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/types-and-class-definitions/static-field-updates.mdx</include>
|
||||
|
||||
<include>../limitations/types-and-class-definitions/array-subclasses.mdx</include>
|
||||
|
||||
<include>../limitations/types-and-class-definitions/local-class-expressions.mdx</include>
|
||||
|
||||
<include>../limitations/types-and-class-definitions/constructor-function-inheritance.mdx</include>
|
||||
@@ -220,6 +222,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.
|
||||
|
||||
@@ -27,20 +27,29 @@
|
||||
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, RSA-PSS, 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, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error.
|
||||
RSA keys generate with modulus lengths from 512 to 8192 bits, an odd length from 2048 bits yielding one bit less as in Node, and odd public exponents up to 4294967295; longer moduli 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.
|
||||
RSA-PSS keys follow their RSASSA-PSS-params as Node does: PSS padding only, the key's digest and MGF1 digest, and salts of at least the key's length, with OpenSSL's errors; their details name the parameters, they export as PKCS#8 and SubjectPublicKeyInfo only, and digests other than SHA-1 and SHA-2 in their options throw a coded SC2020 error.
|
||||
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.
|
||||
Private keys export encrypted (`cipher`, `passphrase`) with aes-128, aes-192, and aes-256 in CBC and ECB modes and with des-ede3-cbc, PKCS#8 as PBES2 with PBKDF2-HMAC-SHA256 and PKCS#1 and SEC1 as traditional PEM, and the ciphers OpenSSL refuses for a form throw its errors; a cipher option can also be an OID; other ciphers Node encrypts with throw a coded SC2020 error.
|
||||
Encrypted private keys import with a `passphrase` from PBES2 with HMAC-SHA PRFs and the CBC and ECB ciphers, and from traditional PEM with the CBC ciphers, with Node's errors for a missing or wrong passphrase; other schemes and ciphers throw a coded SC2020 error once a passphrase is given.
|
||||
The other key types (`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,4 +1,8 @@
|
||||
- **Globals:** Stored `globalThis` references support identity and probes for absent capabilities.
|
||||
Most builtin members require direct global access; access through a stored global object can raise `SC2020`.
|
||||
In JavaScript, reading a builtin global through the global object by name (`globalThis[name]`, or a stored global object) answers what the global's own spelling reads, and builtin module members that are global objects in Node, such as `require("buffer").atob` and `require("timers").setTimeout`, are the same values.
|
||||
`WebAssembly` read this way, and builtin members read through a global object that untyped code received, can raise `SC2020`.
|
||||
`for (key in globalThis)` visits Node's enumerable globals in Node's order, then the program's own global properties.
|
||||
`navigator` answers Node's `hardwareConcurrency`, `language`, `languages`, `platform`, and `userAgent`, with `language` derived from `LC_ALL`, `LC_MESSAGES`, or `LANG` as Node does.
|
||||
`navigator.locks` raises `SC2020`, as does `navigator.language` for a locale outside the `language_TERRITORY.codeset@modifier` form, or one whose language or territory needs ICU's tables (a three-letter territory, for example).
|
||||
`Temporal` is not defined, as in Node.js 24: `typeof Temporal` is `"undefined"`, `globalThis.Temporal` is `undefined`, and any other use throws `ReferenceError: Temporal is not defined`.
|
||||
Programs that name it type-check against TypeScript's `Temporal` declarations.
|
||||
|
||||
@@ -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,14 @@
|
||||
`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.
|
||||
It may also be a source function, which the pipeline calls with `{ signal }` and whose iterable result it pumps, or a `{ readable, writable }` pair, whose readable side it pumps.
|
||||
Later `pipeline` stages that arrive as untyped values may be native streams, `Cipheriv` and `Decipheriv` values, functions, and iterables.
|
||||
A function is called with the previous stage and `{ signal }` as a function stage: before the last stage it must return an async iterable, and as the last stage it may return a promise, whose value resolves the pipeline, or an async iterable, which feeds the PassThrough that `pipeline()` returns.
|
||||
An untyped source function, function stage, or destination that returns a stream is unsupported.
|
||||
An iterable becomes `Duplex.from` of it, which the next stage reads from while the stages before it are not read, as in Node.
|
||||
A pipeline branches at run time on whether each of its first three untyped stages after the first is a function; a function in a later untyped stage, an untyped web stream, and a `{ readable, writable }` pair after the first stage throw a not-supported error.
|
||||
`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,7 +21,7 @@
|
||||
`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`).
|
||||
@@ -27,7 +34,8 @@
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -7,6 +7,7 @@ Some builtins can be stored and passed as functions:
|
||||
|
||||
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
|
||||
Calls through these values bypass `node:test` mock timers.
|
||||
In JavaScript, `atob`, `btoa`, and `structuredClone` are function values the same way, and calls through them run what the direct calls run.
|
||||
Other builtin globals stored in JavaScript remain opaque identity values.
|
||||
In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class.
|
||||
A class can extend `require("events")` directly.
|
||||
@@ -14,7 +15,7 @@ A class can extend `require("events")` directly.
|
||||
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
|
||||
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
|
||||
|
||||
`util.promisify` over an untyped (`any`) function returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`.
|
||||
`util.promisify` over an untyped function (a value typed `any`, a function whose parameters are untyped, or any other target in JavaScript) returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`, and throws Node's `ERR_INVALID_ARG_TYPE` for values that are not functions.
|
||||
Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise.
|
||||
|
||||
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`.
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
`console.log`, `info`, and `debug` write to stdout; `error` and `warn` write to stderr.
|
||||
They accept `unknown`, printing strings verbatim and other values through native `util.inspect`.
|
||||
Direct calls support global `console` and `node:console` imports.
|
||||
Console output, `util.format`, and `util.formatWithOptions` with a format string known only at run time, spread arguments, or an options object format their arguments at run time.
|
||||
There, `%s` of a function or of a class instance whose class defines `toString` and `%o` of an object throw `SC2020`.
|
||||
`util.inspect` with `colors`, with option values known only at run time, or in the `inspect(value, showHidden, depth, colors)` form renders the value's untyped view in the runtime with Node's styles.
|
||||
Colored output of errors, promises, regular expressions, `DataView`, `ArrayBuffer`, weak collections, iterators, and other native objects throws `SC2020`.
|
||||
Inspect options other than `depth`, `colors`, `customInspect`, `compact` (a number or `false`), `breakLength`, and `sorted` (a boolean) throw `SC2020` when they are set to a value other than Node's default.
|
||||
`util.inspect` calls `[util.inspect.custom]` methods of classes and objects with Node's depth, options, and `util.inspect` arguments, and `console.dir` and `customInspect: false` skip them.
|
||||
A class instance with such a method that is held as an untyped value throws `SC2020` when it is inspected, and a class whose subclasses declare the method without declaring it itself is refused.
|
||||
Stored global console values and detached output methods retain identity and accept ordinary arguments.
|
||||
Stored methods do not support format specifiers with substitution arguments.
|
||||
Other stored console APIs, custom `Console` instances, and console mutation are unsupported.
|
||||
|
||||
@@ -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,18 @@ 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`.
|
||||
In JavaScript dependencies, a `createRequire` require call with a computed specifier, and `require.resolve` called with a computed request or read as a value, resolve the specifier with the Node.js algorithm when the program runs.
|
||||
`require.resolve` returns the path Node.js returns.
|
||||
When nothing resolves, both throw the `MODULE_NOT_FOUND` error that Node.js throws, so a package that probes for a missing addon takes the same branch as in Node.js.
|
||||
When the specifier of a require call resolves to a native addon, a builtin, or a file that is not part of the compiled program, the program prints an SC2020 error that names it and exits with status 1, because Node.js would load that module.
|
||||
A `catch` clause around the call does not run in that case.
|
||||
Resolution steps that the runtime does not reproduce, such as package `imports` specifiers, `require.resolve` options, and subpaths that a package does not export, end the program the same way.
|
||||
Coverage lists these calls as deferred to runtime.
|
||||
Other computed uses compile when they are inside a `try` block: a require call with spread arguments, a `require.resolve` call with another argument count, and `require.resolve.paths` called with a computed request evaluate their arguments and then throw an error, and `require.resolve.paths` read as a value is a function that throws when called.
|
||||
Coverage lists these as deferred to runtime as well, and the package's `catch` clause then runs as it does in Node.js when the addon is missing.
|
||||
Outside a `try` block, these uses keep the package refused.
|
||||
This applies until compiled programs can load addons through Node.js.
|
||||
|
||||
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.
|
||||
@@ -23,3 +25,10 @@ Index-signature records list declared fields before the other keys.
|
||||
|
||||
Optional fields and properties added through a record's `unknown` view can be deleted.
|
||||
A required field has no absent state, so deleting it through the view throws `TypeError: Cannot delete property 'x' of #<Object>` and leaves the record unchanged, where Node.js removes the property.
|
||||
|
||||
Records inherit `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf` and `propertyIsEnumerable` from `Object.prototype`.
|
||||
A call of one of these methods by name runs the built-in with the record as its receiver, unless the record holds an own function under that name.
|
||||
A keyed read such as `handlers[name]` of a record whose values are functions answers the built-in when the record lacks the key, so truthiness, `typeof` and calls match Node.js.
|
||||
A keyed read of a record whose values are not functions, such as `counts["toString"]` on a `Record<string, number>`, answers `undefined` where Node.js answers the inherited function, so `counts[word] ?? 0` counts such a word as new.
|
||||
Calling a built-in read from a record of functions throws a `TypeError` when its answer does not fit the record's function type, for example `valueOf` read from a `Record<string, (x: number) => number>`.
|
||||
Other inherited names, such as `constructor`, read `undefined` from typed records.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
- **Array subclasses:** A class that extends `Array` compiles: its instances are arrays, and `map`, `filter`, `slice` and the other methods that create arrays return instances of the subclass, as in Node.js.
|
||||
A generic subclass (`class Bag<T> extends Array<T>`), a class that extends such a subclass, and a subclass that declares `Symbol.species` are unsupported.
|
||||
A method whose result the subclass's constructor cannot construct throws a `TypeError`: the constructor's parameters must take the one length argument, and a `map` to another element type needs a constructor that adds no elements.
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -21382,9 +21382,9 @@
|
||||
"anchor": "err_invalid_mime_syntax",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23520,9 +23520,9 @@
|
||||
"anchor": "err_throttled",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -36684,9 +36684,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "supported",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -36707,9 +36707,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -36730,9 +36730,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -36776,9 +36776,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "supported",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71946,9 +71946,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71969,9 +71969,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71992,9 +71992,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72015,9 +72015,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72038,9 +72038,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72061,9 +72061,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72176,9 +72176,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72199,9 +72199,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72222,9 +72222,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72245,9 +72245,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72268,9 +72268,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72291,9 +72291,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72314,9 +72314,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72337,9 +72337,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72360,9 +72360,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72636,9 +72636,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72751,9 +72751,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72774,9 +72774,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72797,9 +72797,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -73027,9 +73027,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -73050,9 +73050,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -73395,9 +73395,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'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);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.";
|
||||
|
||||
@@ -187,9 +187,22 @@
|
||||
"node-builtin.util.callbackify": ["tests/corpus/5190-util-callbackify.cjs", "tests/corpus/5191-util-callbackify.ts", "tests/fixtures/node-types/util-callables.ts"],
|
||||
"node-builtin.util.debuglog": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts", "tests/fixtures/node-types/util-callables.ts"],
|
||||
"node-builtin.util.debug": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts"],
|
||||
"node-builtin.util.isArray": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
|
||||
"node-builtin.util._extend": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
|
||||
"node-builtin.util.convertProcessSignalToExitCode": ["tests/corpus/7702-util-signal-exit-code.cjs"],
|
||||
"node-builtin.util.aborted": ["tests/corpus/7703-util-aborted.cjs", "tests/corpus/7704-util-aborted.ts"],
|
||||
"node-builtin.util.diff": ["tests/corpus/7705-util-diff.cjs", "tests/corpus/7706-util-diff.ts"],
|
||||
"node-builtin.util.debounce": ["tests/corpus/7710-util-debounce.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
|
||||
"node-builtin.util.throttle": ["tests/corpus/7711-util-throttle.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
|
||||
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
|
||||
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "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"],
|
||||
@@ -202,13 +215,17 @@
|
||||
"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"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] },
|
||||
{ "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
|
||||
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["`hardwareConcurrency` Type: {number}", "`platform` Type: {string}"], "status": "supported", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["`language` Type: {string}", "`languages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/8162-navigator-global.cjs", "tests/fixtures/node-types/navigator.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `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": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] },
|
||||
{"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]},
|
||||
@@ -245,9 +262,11 @@
|
||||
{"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]},
|
||||
{"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]},
|
||||
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
|
||||
{"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]},
|
||||
{"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/7520-util-format-runtime.ts", "tests/corpus/7521-util-format-runtime.cjs"]},
|
||||
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
|
||||
{ "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/7526-inspect-options-runtime.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`util.inspect(object[, showHidden[, depth[, colors]]])`"], "status": "partial", "evidence": ["tests/corpus/7526-inspect-options-runtime.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`custom` Type: {symbol} that can be used to declare custom inspect functions."], "status": "partial", "evidence": ["tests/corpus/7524-inspect-custom.ts", "tests/corpus/7525-inspect-custom.cjs"] },
|
||||
{ "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] },
|
||||
{ "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] },
|
||||
{ "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" },
|
||||
@@ -257,6 +276,10 @@
|
||||
{ "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/corpus/node24-buffer-encoding.ts", "tests/corpus/node24-buffer-encoding-errors.ts", "tests/fixtures/node-types/text-encoding.ts"] },
|
||||
{ "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-text-encode-into.ts", "tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
|
||||
{ "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
|
||||
{ "chapter": "util", "symbols": ["util.MIMEType", "util.MIMEParams", "MIMEType.parse", "mime.toString", "mime.toJSON", "mimeParams.delete", "mimeParams.entries", "mimeParams.get", "mimeParams.has", "mimeParams.keys", "mimeParams.set", "mimeParams.values"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`mimeParams[Symbol.iterator]()`"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_INVALID_MIME_SYNTAX"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_THROTTLED"], "status": "partial", "evidence": ["tests/corpus/7711-util-throttle.cjs"] },
|
||||
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
|
||||
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
|
||||
@@ -332,11 +355,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"] },
|
||||
@@ -401,13 +424,18 @@
|
||||
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "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": "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/corpus/7522-promisify-untyped.cjs", "tests/corpus/7523-promisify-untyped.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"] },
|
||||
@@ -425,6 +453,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"] },
|
||||
@@ -435,11 +468,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"] },
|
||||
|
||||
@@ -17,6 +17,14 @@ set(SCRIPTC_HELPER_ARCH "arm64" CACHE STRING
|
||||
# Linux packages ship this process-isolated helper as a fully static host
|
||||
# executable so it runs independently of the destination libc and libstdc++.
|
||||
option(SCRIPTC_STATIC_LINUX_BUILD "Build a static Linux helper executable" OFF)
|
||||
# The release Linux helpers link an LLVM built with ThinLTO and a profile of
|
||||
# the helper's code generation (llvm-dist.mjs). Its archives hold bitcode, so
|
||||
# the helper compiles and links with ThinLTO too, which needs clang and lld.
|
||||
option(SCRIPTC_LLVM_THINLTO "Compile and link the helper with ThinLTO" OFF)
|
||||
set(SCRIPTC_LLVM_PROFILE_USE "" CACHE FILEPATH
|
||||
"Indexed profile the helper's own sources are optimized with")
|
||||
option(SCRIPTC_LLVM_PROFILE_GENERATE
|
||||
"Instrument the helper's sources to collect that profile" OFF)
|
||||
|
||||
# These must be cache variables before project() enables the Apple toolchain.
|
||||
# A target property named OSX_DEPLOYMENT_TARGET is not recognized by CMake and
|
||||
@@ -219,6 +227,31 @@ else()
|
||||
endif()
|
||||
target_link_libraries(scriptc-llvm-codegen PRIVATE ${SCRIPTC_LLVM_LIBS} ${SCRIPTC_LLVM_SYSTEM_LIBS})
|
||||
|
||||
if(SCRIPTC_LLVM_THINLTO)
|
||||
target_compile_options(scriptc-llvm-codegen PRIVATE -flto=thin)
|
||||
# The thin link optimizes and compiles all of LLVM's code the helper
|
||||
# reaches; the cache makes a relink after a helper edit incremental.
|
||||
target_link_options(scriptc-llvm-codegen PRIVATE
|
||||
-flto=thin
|
||||
-fuse-ld=lld
|
||||
"LINKER:--thinlto-jobs=all"
|
||||
"LINKER:--thinlto-cache-dir=${CMAKE_BINARY_DIR}/thinlto-cache"
|
||||
)
|
||||
endif()
|
||||
if(SCRIPTC_LLVM_PROFILE_USE)
|
||||
# The profile covers LLVM's sources and the helper's. Functions it does
|
||||
# not cover, or whose control flow changed since, are optimized without it.
|
||||
target_compile_options(scriptc-llvm-codegen PRIVATE
|
||||
"-fprofile-use=${SCRIPTC_LLVM_PROFILE_USE}"
|
||||
-Wno-profile-instr-unprofiled
|
||||
-Wno-profile-instr-out-of-date
|
||||
)
|
||||
endif()
|
||||
if(SCRIPTC_LLVM_PROFILE_GENERATE)
|
||||
target_compile_options(scriptc-llvm-codegen PRIVATE -fprofile-generate)
|
||||
target_link_options(scriptc-llvm-codegen PRIVATE -fprofile-generate)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
# Homebrew's static LLVM archives contain broad component object files.
|
||||
# Export only the process entry point so archive-global LLVM symbols do not
|
||||
|
||||
@@ -33,11 +33,11 @@ lowering, verifies before and after optimization, and publishes through a
|
||||
private sibling file so a failed or interrupted request cannot truncate the
|
||||
requested output.
|
||||
|
||||
Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host.
|
||||
Repeating `--output` divides the program into that many partitions, which are compiled concurrently and linked together. The helper splits the emitter's LLVM assembly text directly, so partitions also parse concurrently; input outside the emitter's narrow grammar is parsed whole and split with LLVM instead. Internal definitions that no other definition reaches are left out of every partition, and the others become hidden so partitions can reference each other. At opt-level 0 each partition is compiled independently. Optimized partitions follow the ThinLTO model: each is simplified and summarized on its own thread, a thin link decides which small functions every partition imports for inlining and which hidden definitions become internal again, and each partition is then optimized and compiled on its own thread. Partition placement depends only on the module and the partition count, so output never depends on the host.
|
||||
|
||||
WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
|
||||
|
||||
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect.
|
||||
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect. Partitions drop the names of their struct types before the thin link, which would otherwise number same-named types from different partitions in an order that any edit can shift, changing the module after importing in partitions the edit did not touch.
|
||||
|
||||
`--import-bitcode <unit.bc>`, given once per runtime-pack unit that ships bitcode, imports runtime functions for inlining; scriptc's program builds do not pass it. Small runtime functions reachable from the program's runtime calls are imported as `available_externally` so LLVM can inline them; the runtime objects remain the only definitions. The helper never runs a sanitizer pass, so with import it also drops the emitter's inert `sanitize_address` attributes, which would otherwise block that inlining. Without `--import-bitcode` the module is optimized exactly as emitted.
|
||||
|
||||
@@ -58,3 +58,20 @@ scriptc-llvm-codegen prune-import --input unit.opt.bc --output unit.bc
|
||||
```
|
||||
|
||||
Both commands keep only what a program build can import: the importable bodies within the size limit at their call distance from any public function, and the constants they copy. Every other definition becomes a declaration, so a program imports exactly what it would from the whole unit while reading a fraction of it. Unpromoted unit-local symbols stay local, so release bitcode offers only bodies that need none of them.
|
||||
|
||||
## Linux release builds
|
||||
|
||||
The Linux helper packages are released linked against LLVM 22.1.8 built from its release sources with ThinLTO and with `llvm.profdata.gz`, a profile of the helper's own code generation (`llvm-dist.mjs`). The helper's code generation is 5-9% faster with it than when the helper links LLVM's release archive, and its objects are identical. The release workflow downloads that build from the `LLVM_DIST_TAG` release of this repository; when the archive is missing, the helper links LLVM's release archive instead.
|
||||
|
||||
To build and use it locally, with clang, lld and llvm-profdata 22.1.8 on `PATH` (LLVM's release archive for the host has them):
|
||||
|
||||
```console
|
||||
$ node scripts/build-llvm-dist.mjs --source llvm-project-22.1.8.src --prefix <dir>
|
||||
$ LLVM_DIR=<dir>/lib/cmake/llvm SCRIPTC_LLVM_THINLTO=1 pnpm --filter @scriptc/llvm-linux-x64-gnu build:native
|
||||
```
|
||||
|
||||
`SCRIPTC_LLVM_THINLTO=1` compiles and links the helper with clang, lld and ThinLTO, since that LLVM's archives hold bitcode, and optimizes the helper's own sources with the same profile. The link takes about 4 minutes on 4 cores.
|
||||
|
||||
The `llvm-dist` workflow (run by hand) builds the archive for x64 and arm64, checks that a helper linked against it gives the same objects as one linked against LLVM's release archive (`scripts/check-llvm-dist.mjs`), and attaches it to the release. The arm64 build uses the profile collected on x64, and its log reports how many functions the profile does not match there.
|
||||
|
||||
The profile depends on LLVM's sources and the kind of code scriptc emits, not on scriptc's commits. `scripts/refresh-llvm-profile.mjs` collects a new one after an LLVM version change: it builds LLVM with IR instrumentation and an instrumented helper, runs that helper on tsc-ts, five Effect fixtures, the runtime benchmark workloads and every 50th corpus program (`scripts/llvm-helper-inputs.mjs`), and merges the result. A new profile or LLVM version gets a new `LLVM_DIST_TAG`.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/* The static Linux helper build shared by the four Linux helper packages,
|
||||
* scripts/refresh-llvm-profile.mjs and scripts/check-llvm-dist.mjs. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { helperLlvmDistArgs } from "./llvm-dist.mjs";
|
||||
import { nativeTargetCmakeArgs } from "./targets.mjs";
|
||||
|
||||
const DATA_LAYOUTS = {
|
||||
x64: "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
|
||||
arm64:
|
||||
"e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32",
|
||||
};
|
||||
|
||||
/** Configures and builds the helper for `arch` (`x64` or `arm64`) and `libc`
|
||||
* (`gnu` or `musl`) in `build` against LLVM_DIR, and writes the stripped
|
||||
* executable to `output`. musl builds take the compilers from CC and CXX. */
|
||||
export function buildLinuxHelper({ arch, libc, version, build, output }) {
|
||||
const triple = `${arch === "x64" ? "x86_64" : "aarch64"}-unknown-linux-${libc}`;
|
||||
// CMake drops the other definitions when a reconfigure changes the
|
||||
// compiler, so a build directory configured for another LLVM or build
|
||||
// kind starts over.
|
||||
const kind = JSON.stringify({
|
||||
llvm: process.env.LLVM_DIR ?? null,
|
||||
thinLTO: process.env.SCRIPTC_LLVM_THINLTO === "1",
|
||||
profileGenerate: process.env.SCRIPTC_LLVM_PROFILE_GENERATE === "1",
|
||||
cc: process.env.CC ?? null,
|
||||
cxx: process.env.CXX ?? null,
|
||||
});
|
||||
const stamp = join(build, "scriptc-helper-build.json");
|
||||
if (existsSync(build) && (!existsSync(stamp) || readFileSync(stamp, "utf8") !== kind))
|
||||
rmSync(build, { recursive: true, force: true });
|
||||
mkdirSync(build, { recursive: true });
|
||||
writeFileSync(stamp, kind);
|
||||
execFileSync(
|
||||
"cmake",
|
||||
[
|
||||
"-S",
|
||||
fileURLToPath(new URL(".", import.meta.url)),
|
||||
"-B",
|
||||
build,
|
||||
"-G",
|
||||
"Ninja",
|
||||
...(libc === "musl"
|
||||
? [
|
||||
`-DCMAKE_C_COMPILER=${process.env.CC ?? "cc"}`,
|
||||
`-DCMAKE_CXX_COMPILER=${process.env.CXX ?? "c++"}`,
|
||||
]
|
||||
: []),
|
||||
`-DLLVM_DIR=${process.env.LLVM_DIR ?? "/usr/lib/llvm-22/lib/cmake/llvm"}`,
|
||||
`-DSCRIPTC_PACKAGE_VERSION=${version}`,
|
||||
`-DSCRIPTC_DEFAULT_TARGET=${triple}`,
|
||||
`-DSCRIPTC_DEFAULT_DATA_LAYOUT=${DATA_LAYOUTS[arch]}`,
|
||||
...nativeTargetCmakeArgs,
|
||||
"-DSCRIPTC_STATIC_LINUX_BUILD=ON",
|
||||
"-DCMAKE_EXE_LINKER_FLAGS=-static",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
// Later definitions win, so these can select clang for musl builds.
|
||||
...helperLlvmDistArgs(build),
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
execFileSync("cmake", ["--build", build, "--target", "scriptc-llvm-codegen"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
mkdirSync(dirname(output), { recursive: true });
|
||||
copyFileSync(join(build, "scriptc-llvm-codegen"), output);
|
||||
execFileSync("strip", [output], { stdio: "inherit" });
|
||||
chmodSync(output, 0o755);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/* The LLVM distribution the Linux helpers link in releases: LLVM built from
|
||||
* the release sources with ThinLTO and with a profile of the helper's own
|
||||
* code generation (llvm.profdata.gz). scripts/build-llvm-dist.mjs builds it,
|
||||
* the llvm-dist workflow attaches it to the LLVM_DIST_TAG release of this
|
||||
* repository, and scripts/refresh-llvm-profile.mjs collects the profile. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
|
||||
export const LLVM_VERSION = "22.1.8";
|
||||
/** The release of this repository that holds the distribution archives. A
|
||||
* new profile or LLVM version gets a new tag. */
|
||||
export const LLVM_DIST_TAG = "llvm-22.1.8-pgo.1";
|
||||
export const llvmProfile = fileURLToPath(new URL("./llvm.profdata.gz", import.meta.url));
|
||||
|
||||
/** The archive name for a Linux architecture (`x64` or `arm64`). */
|
||||
export function llvmDistAsset(arch) {
|
||||
return `scriptc-llvm-${LLVM_VERSION}-linux-${arch}.tar.xz`;
|
||||
}
|
||||
|
||||
/** Writes the uncompressed profile into `dir` and returns its path. */
|
||||
export function writeLlvmProfile(dir, profile = llvmProfile) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const path = join(dir, "llvm.profdata");
|
||||
writeFileSync(path, gunzipSync(readFileSync(profile)));
|
||||
return path;
|
||||
}
|
||||
|
||||
/** CMake arguments for an LLVM build with the helper's targets, static,
|
||||
* without tools or tests. `profile` builds with that indexed profile and
|
||||
* ThinLTO; `instrumented` builds the IR-instrumented libraries the profile
|
||||
* is collected with. clang and lld come from PATH and must be LLVM_VERSION,
|
||||
* since the profile's format and hashes follow the compiler. */
|
||||
export function llvmCmakeArgs({ source, build, prefix, profile, instrumented }) {
|
||||
const version = execFileSync("clang", ["--version"], { encoding: "utf8" });
|
||||
if (!version.includes(`clang version ${LLVM_VERSION}`))
|
||||
throw new Error(`the LLVM distribution is built with clang ${LLVM_VERSION}; found ${version}`);
|
||||
return [
|
||||
"-S",
|
||||
join(source, "llvm"),
|
||||
"-B",
|
||||
build,
|
||||
"-G",
|
||||
"Ninja",
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
`-DCMAKE_INSTALL_PREFIX=${prefix}`,
|
||||
"-DCMAKE_C_COMPILER=clang",
|
||||
"-DCMAKE_CXX_COMPILER=clang++",
|
||||
"-DLLVM_USE_LINKER=lld",
|
||||
"-DLLVM_TARGETS_TO_BUILD=X86;AArch64;WebAssembly",
|
||||
"-DLLVM_ENABLE_PROJECTS=",
|
||||
"-DLLVM_ENABLE_ASSERTIONS=OFF",
|
||||
"-DLLVM_ENABLE_ZLIB=FORCE_ON",
|
||||
"-DLLVM_ENABLE_ZSTD=FORCE_ON",
|
||||
"-DLLVM_USE_STATIC_ZSTD=ON",
|
||||
"-DLLVM_ENABLE_TERMINFO=OFF",
|
||||
"-DLLVM_ENABLE_LIBXML2=OFF",
|
||||
"-DLLVM_ENABLE_LIBEDIT=OFF",
|
||||
"-DLLVM_ENABLE_LIBPFM=OFF",
|
||||
"-DLLVM_INCLUDE_TESTS=OFF",
|
||||
"-DLLVM_INCLUDE_EXAMPLES=OFF",
|
||||
"-DLLVM_INCLUDE_BENCHMARKS=OFF",
|
||||
"-DLLVM_INCLUDE_DOCS=OFF",
|
||||
"-DLLVM_BUILD_TOOLS=OFF",
|
||||
...(instrumented ? ["-DLLVM_BUILD_INSTRUMENTED=IR", "-DLLVM_BUILD_RUNTIME=OFF"] : []),
|
||||
...(profile ? ["-DLLVM_ENABLE_LTO=Thin", `-DLLVM_PROFDATA_FILE=${profile}`] : []),
|
||||
];
|
||||
}
|
||||
|
||||
/** Extra CMake arguments for a Linux helper package build. With
|
||||
* SCRIPTC_LLVM_THINLTO=1, LLVM_DIR names a distribution from
|
||||
* scripts/build-llvm-dist.mjs: its archives hold ThinLTO bitcode, so the
|
||||
* helper compiles and links with clang, lld and ThinLTO, and its own sources
|
||||
* use the same profile. SCRIPTC_LLVM_PROFILE_GENERATE=1 instruments the
|
||||
* helper's sources against an instrumented LLVM. */
|
||||
export function helperLlvmDistArgs(build) {
|
||||
const args = [];
|
||||
if (process.env.SCRIPTC_LLVM_THINLTO === "1")
|
||||
args.push(
|
||||
"-DCMAKE_C_COMPILER=clang",
|
||||
"-DCMAKE_CXX_COMPILER=clang++",
|
||||
"-DSCRIPTC_LLVM_THINLTO=ON",
|
||||
`-DSCRIPTC_LLVM_PROFILE_USE=${writeLlvmProfile(build)}`,
|
||||
);
|
||||
else args.push("-DSCRIPTC_LLVM_THINLTO=OFF", "-DSCRIPTC_LLVM_PROFILE_USE=");
|
||||
if (process.env.SCRIPTC_LLVM_PROFILE_GENERATE === "1")
|
||||
args.push(
|
||||
"-DCMAKE_C_COMPILER=clang",
|
||||
"-DCMAKE_CXX_COMPILER=clang++",
|
||||
"-DSCRIPTC_LLVM_PROFILE_GENERATE=ON",
|
||||
);
|
||||
else args.push("-DSCRIPTC_LLVM_PROFILE_GENERATE=OFF");
|
||||
return args;
|
||||
}
|
||||
Binary file not shown.
@@ -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>
|
||||
@@ -61,6 +64,9 @@ static constexpr size_t MaxPartitions = 64;
|
||||
// multiple of the usual size limit, in builds with a profile. See
|
||||
// limitProfileImports().
|
||||
static constexpr const char *ProfileImportMultiplier = "2";
|
||||
// The thin link's size limit, in instructions, for a callee it imports into
|
||||
// a partition that calls it. See limitImports().
|
||||
static constexpr const char *ImportInstructionLimit = "40";
|
||||
|
||||
std::optional<EmitOptions> parseEmitOptions(int Argc, char **Argv) {
|
||||
EmitOptions Options;
|
||||
@@ -170,6 +176,141 @@ void limitProfileImports() {
|
||||
(void)Limited;
|
||||
}
|
||||
|
||||
// The thin link imports a callee of up to 100 instructions, LLVM's default,
|
||||
// into every partition that calls it, and each partition then optimizes and
|
||||
// compiles its imported copies again. On tsc-ts (64 partitions) a limit of 40
|
||||
// takes about 7% off the helper's time, and the program runs as fast; 30 and
|
||||
// below made it run 1-8% slower. The limit is a process-wide option; the
|
||||
// helper sets it once.
|
||||
void limitImports() {
|
||||
static const bool Limited = [] {
|
||||
if (cl::Option *Option =
|
||||
cl::getRegisteredOptions().lookup("import-instr-limit"))
|
||||
Option->addOccurrence(0, "import-instr-limit", ImportInstructionLimit);
|
||||
return true;
|
||||
}();
|
||||
(void)Limited;
|
||||
}
|
||||
|
||||
// Drops the names of a partition's struct types before its summary is
|
||||
// written. Every partition declares the program's types under the same
|
||||
// names, and a thin link loads the importing partition and the partitions
|
||||
// it imports from into one context, which tells same-named types apart by
|
||||
// appending a counter that runs across the whole link. An edit that adds a
|
||||
// type anywhere shifts the counter, so the module after importing, the
|
||||
// object cache key, changed in partitions whose code had not. Unnamed types
|
||||
// carry no such suffix; importing still matches them by their bodies.
|
||||
struct UnnameStructTypes : PassInfoMixin<UnnameStructTypes> {
|
||||
PreservedAnalyses run(Module &M, ModuleAnalysisManager &) {
|
||||
for (StructType *Type : M.getIdentifiedStructTypes())
|
||||
Type->setName("");
|
||||
return PreservedAnalyses::all();
|
||||
}
|
||||
static bool isRequired() { return true; }
|
||||
};
|
||||
|
||||
// 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 +332,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 +341,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 +542,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 +717,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 +766,20 @@ 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(UnnameStructTypes());
|
||||
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);
|
||||
@@ -689,6 +871,7 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
std::vector<std::string> Names(Count);
|
||||
for (size_t I = 0; I < Count; ++I)
|
||||
Names[I] = "partition-" + std::to_string(I);
|
||||
limitImports();
|
||||
if (!Options.ProfileUse.empty())
|
||||
limitProfileImports();
|
||||
lto::LTO Link(std::move(Config),
|
||||
@@ -744,7 +927,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 +962,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 +1003,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 +1016,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 +1038,8 @@ int emitPartitions(const EmitOptions &Options) {
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
}
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportProfileMatch(*Match);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -895,6 +1084,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 +1148,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 +1160,8 @@ int emit(const EmitOptions &Options) {
|
||||
return reportError(Failure->Code, Failure->Message,
|
||||
Options.DiagnosticFormat);
|
||||
}
|
||||
if (!Options.ProfileUse.empty())
|
||||
reportProfileMatch(Match);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,85 @@ 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 || S.Partition == NoPartition)
|
||||
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];
|
||||
if (S.Kind != SymbolKind::Function || S.Partition == NoPartition)
|
||||
continue;
|
||||
StringRef Group = Symbols[Roots[I]].Group;
|
||||
if (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 +409,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 +423,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
|
||||
@@ -443,61 +536,6 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
}
|
||||
}
|
||||
|
||||
// An internal function referenced by exactly one other function, and by
|
||||
// no global, is a whole-program inlining candidate; keep it with that
|
||||
// function so its partition can inline it without importing. A function
|
||||
// body larger than this is past LLVM's inline cost limit even for the only
|
||||
// call to a local function, so it is placed by its own name. Otherwise a
|
||||
// program's module initializers, which only the entry point calls, would
|
||||
// all share the entry point's partition.
|
||||
constexpr size_t MaxInlinedBodyBytes = 256 * 1024;
|
||||
constexpr unsigned Shared = ~1u;
|
||||
std::vector<unsigned> Referrer(Symbols.size(), NoPartition);
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
if (S.Kind == SymbolKind::Declaration)
|
||||
continue;
|
||||
for (unsigned Ref : S.Refs) {
|
||||
if (Ref == I)
|
||||
continue;
|
||||
if (S.Kind == SymbolKind::Variable || Referrer[Ref] != NoPartition)
|
||||
Referrer[Ref] = Shared;
|
||||
else
|
||||
Referrer[Ref] = I;
|
||||
}
|
||||
}
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
if (Symbols[I].Kind != SymbolKind::Function)
|
||||
continue;
|
||||
unsigned Root = I;
|
||||
for (unsigned Steps = 0; Steps < 1024; ++Steps) {
|
||||
const Symbol &S = Symbols[Root];
|
||||
unsigned Parent = Referrer[Root];
|
||||
if (!S.Local || S.Body.size() > MaxInlinedBodyBytes ||
|
||||
Parent == NoPartition || Parent == Shared ||
|
||||
Symbols[Parent].Kind != SymbolKind::Function || Parent == I)
|
||||
break;
|
||||
Root = Parent;
|
||||
}
|
||||
Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count;
|
||||
}
|
||||
for (const Symbol &S : Symbols) {
|
||||
if (S.Kind != SymbolKind::Function)
|
||||
continue;
|
||||
for (unsigned Ref : S.Refs)
|
||||
if (Symbols[Ref].Kind == SymbolKind::Variable &&
|
||||
Symbols[Ref].Partition == NoPartition)
|
||||
Symbols[Ref].Partition = S.Partition;
|
||||
}
|
||||
for (Symbol &S : Symbols)
|
||||
if (S.Kind == SymbolKind::Variable && S.Partition == NoPartition)
|
||||
S.Partition = stableHash(S.Name) % Count;
|
||||
|
||||
ProgramPartitions Result;
|
||||
for (const Symbol &S : Symbols)
|
||||
if (S.Kind != SymbolKind::Declaration && !S.Local)
|
||||
Result.PublicSymbols.insert(S.Name);
|
||||
|
||||
// Metadata nodes are numbered in the emitter's global order, so one added
|
||||
// node would renumber the references in every partition. Each partition
|
||||
// instead numbers the nodes it uses in order of first use, which keeps an
|
||||
@@ -512,10 +550,122 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
for (StringRef Line : NamedMetadata)
|
||||
scanReferences(Line, Index, NamedSymbols, NamedRoots);
|
||||
|
||||
// An internal definition that no other definition reaches is left out of
|
||||
// every partition: the optimizer would delete it, and the emitter writes
|
||||
// variants of helpers that the program never calls. Leaving them out
|
||||
// shortens every partition's parse, summary and optimization, and since
|
||||
// placement ignores them too, an edit that adds one changes no partition.
|
||||
std::vector<char> Live(Symbols.size(), 0);
|
||||
{
|
||||
std::vector<unsigned> Work;
|
||||
auto Reach = [&](unsigned I) {
|
||||
if (!Live[I]) {
|
||||
Live[I] = 1;
|
||||
Work.push_back(I);
|
||||
}
|
||||
};
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Kind != SymbolKind::Declaration && !Symbols[I].Local)
|
||||
Reach(I);
|
||||
for (unsigned I : NamedSymbols)
|
||||
Reach(I);
|
||||
for (auto &Entry : Metadata)
|
||||
for (unsigned I : Entry.second.Symbols)
|
||||
Reach(I);
|
||||
while (!Work.empty()) {
|
||||
unsigned I = Work.back();
|
||||
Work.pop_back();
|
||||
for (unsigned Ref : Symbols[I].Refs)
|
||||
Reach(Ref);
|
||||
}
|
||||
}
|
||||
|
||||
// An internal definition reached from exactly one other definition is an
|
||||
// inlining candidate of it, or data only it uses; keep it with that
|
||||
// definition so its partition can inline it without importing, and so a
|
||||
// helper an edit adds lands in the partition of the code that uses it. A
|
||||
// chain through a global continues to the function that uses the global.
|
||||
// A function body larger than this is past LLVM's inline cost limit even
|
||||
// for the only call to a local function, so it is placed by its own name.
|
||||
// A chain stops where it would leave a function's source-module group:
|
||||
// module initializers, which only the entry point calls, keep their own
|
||||
// partitions instead of all sharing the entry point's.
|
||||
constexpr size_t MaxInlinedBodyBytes = 256 * 1024;
|
||||
constexpr unsigned Shared = ~1u;
|
||||
std::vector<unsigned> Referrer(Symbols.size(), NoPartition);
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
if (S.Kind == SymbolKind::Declaration || !Live[I])
|
||||
continue;
|
||||
for (unsigned Ref : S.Refs) {
|
||||
if (Ref == I)
|
||||
continue;
|
||||
Referrer[Ref] = Referrer[Ref] == NoPartition ? I : Shared;
|
||||
}
|
||||
}
|
||||
auto Follows = [&](unsigned Child, unsigned Parent) {
|
||||
const Symbol &S = Symbols[Child];
|
||||
if (!S.Local || Parent == NoPartition || Parent == Shared ||
|
||||
Symbols[Parent].Kind == SymbolKind::Declaration)
|
||||
return false;
|
||||
if (S.Kind == SymbolKind::Function &&
|
||||
(S.Body.size() > MaxInlinedBodyBytes ||
|
||||
(Symbols[Parent].Kind == SymbolKind::Function && !S.Group.empty() &&
|
||||
S.Group != Symbols[Parent].Group)))
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
std::vector<unsigned> Roots(Symbols.size());
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
if (Symbols[I].Kind == SymbolKind::Declaration || !Live[I])
|
||||
continue;
|
||||
unsigned Root = I;
|
||||
for (unsigned Steps = 0; Steps < 1024; ++Steps) {
|
||||
unsigned Parent = Referrer[Root];
|
||||
if (!Follows(Root, Parent) || Parent == I)
|
||||
break;
|
||||
Root = Parent;
|
||||
}
|
||||
// A function whose chain ends at a global that is not itself placed
|
||||
// with a function (a table many functions use) is placed by its own
|
||||
// name, so the functions of one table spread over the partitions.
|
||||
if (Symbols[I].Kind == SymbolKind::Function &&
|
||||
Symbols[Root].Kind != SymbolKind::Function)
|
||||
Root = I;
|
||||
Roots[I] = Root;
|
||||
if (Symbols[I].Kind == SymbolKind::Function)
|
||||
Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count;
|
||||
}
|
||||
placeGroups(Symbols, Roots, Count);
|
||||
// A global follows the function its chain ends at, then the first
|
||||
// function that references it, then its own name.
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Kind == SymbolKind::Variable && Live[I] &&
|
||||
Symbols[Roots[I]].Kind == SymbolKind::Function)
|
||||
Symbols[I].Partition = Symbols[Roots[I]].Partition;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
const Symbol &S = Symbols[I];
|
||||
if (S.Kind != SymbolKind::Function || !Live[I])
|
||||
continue;
|
||||
for (unsigned Ref : S.Refs)
|
||||
if (Symbols[Ref].Kind == SymbolKind::Variable &&
|
||||
Symbols[Ref].Partition == NoPartition)
|
||||
Symbols[Ref].Partition = S.Partition;
|
||||
}
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Kind == SymbolKind::Variable && Live[I] &&
|
||||
Symbols[I].Partition == NoPartition)
|
||||
Symbols[I].Partition = stableHash(Symbols[I].Name) % Count;
|
||||
|
||||
ProgramPartitions Result;
|
||||
for (const Symbol &S : Symbols)
|
||||
if (S.Kind != SymbolKind::Declaration && !S.Local)
|
||||
Result.PublicSymbols.insert(S.Name);
|
||||
|
||||
Result.Sources.resize(Count);
|
||||
std::vector<std::vector<unsigned>> Owned(Count);
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Kind != SymbolKind::Declaration)
|
||||
if (Symbols[I].Kind != SymbolKind::Declaration && Live[I])
|
||||
Owned[Symbols[I].Partition].push_back(I);
|
||||
|
||||
auto Assemble = [&](unsigned P) {
|
||||
|
||||
@@ -20,11 +20,19 @@ 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
|
||||
// definition referenced by exactly one other definition stays with it, also
|
||||
// through a global, unless it is a function whose body is too large to
|
||||
// inline or that would join a function of another source-module group; a
|
||||
// global otherwise stays with the first function that references it.
|
||||
// Internal definitions that no non-internal definition, named metadata or
|
||||
// metadata node reaches are left out of every partition. 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);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"test:ts7": "node scripts/test-ts7.mjs",
|
||||
"test:checker-identity": "node scripts/checker-identity.mjs",
|
||||
"generate:ts7-ast": "node scripts/generate-ts7-ast-schema.mjs",
|
||||
"generate:ir-fields": "node scripts/generate-ir-fields.mjs",
|
||||
"test:fetch-conformance": "vitest run tests/harness/fetch-conformance.test.ts",
|
||||
"test:test262": "tsx tests/test262/run.ts",
|
||||
"test:test262:fetch": "node tests/test262/fetch.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-arm64",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for darwin-arm64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -18,9 +18,13 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@scriptc/llvm-darwin-arm64": "workspace:*",
|
||||
"@scriptc/runtime-darwin-arm64": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-x64",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for darwin-x64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -18,9 +18,13 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@scriptc/llvm-darwin-x64": "workspace:*",
|
||||
"@scriptc/runtime-darwin-x64": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-gnu",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-arm64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -18,9 +18,13 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@scriptc/llvm-linux-arm64-gnu": "workspace:*",
|
||||
"@scriptc/runtime-linux-arm64-gnu": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-musl",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-arm64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -18,9 +18,13 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@scriptc/llvm-linux-arm64-musl": "workspace:*",
|
||||
"@scriptc/runtime-linux-arm64-musl": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-gnu",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-x64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -18,9 +18,13 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@scriptc/llvm-linux-x64-gnu": "workspace:*",
|
||||
"@scriptc/runtime-linux-x64-gnu": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-musl",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-x64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -18,9 +18,13 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@scriptc/llvm-linux-x64-musl": "workspace:*",
|
||||
"@scriptc/runtime-linux-x64-musl": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-win32-x64-msvc",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for win32-x64-msvc",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -18,9 +18,13 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@scriptc/llvm-win32-x64-msvc": "workspace:*",
|
||||
"@scriptc/runtime-win32-x64-msvc": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"\"",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist",
|
||||
"build:native": "node --max-old-space-size=8192 --import tsx ../../scripts/build-native-cli.mts dist --platform-packages",
|
||||
"prepack": "node ../../scripts/verify-native-cli.mjs ."
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scriptc",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.5",
|
||||
"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",
|
||||
|
||||
@@ -49,6 +49,7 @@ export function relocateToolchain(manifest, sourceDirectory, destinationDirector
|
||||
"declarations",
|
||||
"comptime",
|
||||
"wasi_node_runner",
|
||||
"lib_diagnostics",
|
||||
]) {
|
||||
if (typeof relocated[name] === "string") relocated[name] = pathFrom(relocated[name]);
|
||||
}
|
||||
@@ -92,6 +93,34 @@ export function installNativeCli(
|
||||
const bin = join(directory, "bin");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
const manifest = relocateToolchain(original, sourceDirectory, bin);
|
||||
// A platform package depends on its LLVM helper and runtime pack packages
|
||||
// instead of bundling them. Record where the package manager put them.
|
||||
if (manifest.llvm_package === undefined || manifest.runtime_pack === undefined) {
|
||||
const dependencies = Object.keys(identity.dependencies ?? {});
|
||||
const installed = (prefix) => {
|
||||
const name = dependencies.find((dependency) => dependency.startsWith(prefix));
|
||||
if (name === undefined)
|
||||
throw new Error(`${packageName} does not depend on a ${prefix}* package`);
|
||||
let path, found;
|
||||
try {
|
||||
path = createRequire(platformManifest).resolve(`${name}/package.json`);
|
||||
found = JSON.parse(readFileSync(path, "utf8")).version;
|
||||
} catch {
|
||||
throw new Error(`scriptc requires ${name}@${version}; reinstall scriptc`);
|
||||
}
|
||||
if (found !== version)
|
||||
throw new Error(`scriptc requires ${name}@${version}, found ${found}; reinstall scriptc`);
|
||||
return relative(bin, dirname(path));
|
||||
};
|
||||
manifest.llvm_package ??= installed("@scriptc/llvm-");
|
||||
if (manifest.runtime_pack === undefined) {
|
||||
manifest.runtime_pack = installed("@scriptc/runtime-");
|
||||
manifest.runtime_packs = [
|
||||
{ target: manifest.target, path: manifest.runtime_pack },
|
||||
...(manifest.runtime_packs ?? []),
|
||||
];
|
||||
}
|
||||
}
|
||||
// npm normalizes modes for payloads outside package bin entries. These
|
||||
// tools are launched directly by the compiler after installation.
|
||||
if (process.platform !== "win32") {
|
||||
|
||||
@@ -73,6 +73,8 @@ async function tryFastPath(): Promise<number | null> {
|
||||
// The full compiler keys these builds by the recorded libnode pack.
|
||||
values.libnode !== undefined ||
|
||||
process.env["SCRIPTC_LIBNODE"] !== undefined ||
|
||||
// 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)
|
||||
@@ -142,8 +144,14 @@ async function tryFastPath(): Promise<number | null> {
|
||||
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
|
||||
...(values.strip ? { strip: true as const } : {}),
|
||||
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
|
||||
...(values["cpu-dispatch"] === true ? { cpuDispatch: true as const } : {}),
|
||||
...(checker === undefined ? {} : { checker }),
|
||||
npmStatic,
|
||||
// Mirrors the full compiler's key: shape inference's overlay is opt-in
|
||||
// (SCRIPTC_SHAPE_OVERLAY=1) and --no-shape-overlay turns it off.
|
||||
...(process.env["SCRIPTC_SHAPE_OVERLAY"] === "1" && values["shape-overlay"] !== false
|
||||
? { shapeOverlay: true as const }
|
||||
: {}),
|
||||
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
|
||||
target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${
|
||||
helperObjectRoute ? "runtime-pack" : "driver-tu"
|
||||
@@ -161,6 +169,8 @@ async function tryFastPath(): Promise<number | null> {
|
||||
)
|
||||
: startup.executableNativeEnvironmentFingerprint(),
|
||||
nodeVersion: process.version,
|
||||
// An entry of a program that calls into Node.js serves only the same pack.
|
||||
libnodeIdentity: () => startup.libnodeCacheKey(values.libnode),
|
||||
});
|
||||
if (hit === null) return null;
|
||||
if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true });
|
||||
@@ -220,6 +230,8 @@ async function coverageFastPath(
|
||||
values.emit !== undefined ||
|
||||
values.strip ||
|
||||
values["detached-exit"] ||
|
||||
values["target-cpu"] !== undefined ||
|
||||
values["cpu-dispatch"] !== undefined ||
|
||||
values.lib ||
|
||||
values.profile !== undefined ||
|
||||
values.ffi !== undefined ||
|
||||
@@ -229,6 +241,7 @@ async function coverageFastPath(
|
||||
values["provenance-sources"] ||
|
||||
values["emit-ir"] ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
values["shape-overlay"] === false ||
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
// The replay covers the default Node island policy only.
|
||||
hasNodeIslandFlags(values)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
mergeProfiles,
|
||||
resolveProvenanceSources,
|
||||
sourceTargetPlatform,
|
||||
typePatches,
|
||||
warmNativeCaches,
|
||||
} from "@scriptc/compiler";
|
||||
import { runCli } from "./command.js";
|
||||
@@ -39,6 +40,7 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
||||
sourceTargetPlatform,
|
||||
warmNativeCaches,
|
||||
mergeProfiles,
|
||||
typePatches,
|
||||
run: (binary) =>
|
||||
new Promise<number>((resolveExit) => {
|
||||
let child;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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";
|
||||
|
||||
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 program = `function mix(a: Float64Array, b: Float64Array, k: number): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < a.length; i++) s += a[i]! * k + b[i]! / (i + 1);
|
||||
return s;
|
||||
}
|
||||
const a = new Float64Array(4096);
|
||||
const b = new Float64Array(4096);
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
a[i] = ((i * 7919) % 1000) / 7 - 50;
|
||||
b[i] = 1 / (i + 3);
|
||||
}
|
||||
let total = 0;
|
||||
for (let r = 0; r < 32; r++) total += mix(a, b, r * 0.1);
|
||||
console.log(total, [3, 1, 2].map((x) => mix(a.subarray(x), b, x)).join(" "));
|
||||
`;
|
||||
|
||||
test.runIf(process.platform === "linux" && process.arch === "x64")(
|
||||
"--cpu-dispatch builds one executable that matches Node at every level",
|
||||
async () => {
|
||||
const dir = await mkdtemp(join("/tmp", "scriptc-cpu-dispatch-"));
|
||||
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");
|
||||
};
|
||||
const run = async (level?: string): Promise<string> =>
|
||||
(
|
||||
await execFileAsync(outPath, [], {
|
||||
env: { ...process.env, ...(level === undefined ? {} : { SCRIPTC_CPU_LEVEL: level }) },
|
||||
})
|
||||
).stdout;
|
||||
try {
|
||||
await writeFile(entry, program);
|
||||
const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
|
||||
const plain = await build(["--optimization=speed"]);
|
||||
expect(plain).not.toContain("@sc_cpu_level");
|
||||
const dispatching = await build(["--optimization=speed", "--cpu-dispatch"]);
|
||||
expect(dispatching).toMatch(/^define internal double @sc_bf_mix\.v3\(/m);
|
||||
for (const level of [undefined, "0", "baseline", "3", "x86-64-v3"])
|
||||
expect(await run(level), `SCRIPTC_CPU_LEVEL=${level}`).toBe(expected);
|
||||
// The bootstrap's cached route keeps the two builds apart.
|
||||
expect(await build(["--optimization=speed"], true)).toBe(plain);
|
||||
expect(await build(["--optimization=speed", "--cpu-dispatch"], true)).toBe(dispatching);
|
||||
expect(await build(["--cpu-dispatch"])).toContain("@sc_cpu_level_init()");
|
||||
expect(await run("0")).toBe(expected);
|
||||
expect(await build(["--target-cpu=x86-64-v3", "--cpu-dispatch"])).not.toContain(
|
||||
"@sc_cpu_level",
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
);
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -53,6 +54,7 @@ function fixture() {
|
||||
dsymutil: "dsymutil",
|
||||
comptime: "../lib/comptime",
|
||||
wasi_node_runner: "../lib/wasi/cli/wasi-runner.js",
|
||||
lib_diagnostics: "../lib/lib-diagnostics.json",
|
||||
};
|
||||
writeFileSync(binary + ".json", JSON.stringify(toolchain));
|
||||
const helpers = [
|
||||
@@ -93,6 +95,9 @@ test("installs a direct executable and relocatable references to platform assets
|
||||
expect(resolve(dirname(installed), manifest.wasi_node_runner)).toBe(
|
||||
join(platform, "wasi/cli/wasi-runner.js"),
|
||||
);
|
||||
expect(resolve(dirname(installed), manifest.lib_diagnostics)).toBe(
|
||||
join(platform, "lib-diagnostics.json"),
|
||||
);
|
||||
expect(resolve(dirname(installed), manifest.runtime_packs[0].path)).toBe(
|
||||
join(platform, "runtime"),
|
||||
);
|
||||
@@ -100,6 +105,71 @@ test("installs a direct executable and relocatable references to platform assets
|
||||
expect(JSON.stringify(manifest)).not.toContain(f.directory);
|
||||
});
|
||||
|
||||
test.each(["hoisted", "pnpm"])(
|
||||
"resolves the LLVM helper and runtime pack a platform package depends on (%s)",
|
||||
(layout) => {
|
||||
const f = fixture();
|
||||
const helper = "@scriptc/llvm-darwin-arm64";
|
||||
const runtime = "@scriptc/runtime-darwin-arm64";
|
||||
const toolchain: Record<string, unknown> = { ...f.toolchain };
|
||||
delete toolchain.llvm_package;
|
||||
delete toolchain.runtime_pack;
|
||||
delete toolchain.runtime_packs;
|
||||
writeFileSync(f.binary + ".json", JSON.stringify(toolchain));
|
||||
// pnpm links a package's dependencies beside it in its virtual store,
|
||||
// where the installation's node_modules does not see them.
|
||||
const platform =
|
||||
layout === "pnpm"
|
||||
? join(f.directory, "node_modules/.pnpm/cli@1.2.3/node_modules", f.packageName)
|
||||
: f.platform;
|
||||
if (layout === "pnpm") {
|
||||
mkdirSync(dirname(platform), { recursive: true });
|
||||
renameSync(f.platform, platform);
|
||||
symlinkSync(platform, f.platform, "junction");
|
||||
}
|
||||
const modules =
|
||||
layout === "pnpm" ? dirname(dirname(platform)) : join(f.directory, "node_modules");
|
||||
writeFileSync(
|
||||
join(platform, "package.json"),
|
||||
JSON.stringify({
|
||||
name: f.packageName,
|
||||
version: "1.2.3",
|
||||
dependencies: { [helper]: "1.2.3", [runtime]: "1.2.3" },
|
||||
}),
|
||||
);
|
||||
for (const name of [helper, runtime]) {
|
||||
mkdirSync(join(modules, name), { recursive: true });
|
||||
writeFileSync(
|
||||
join(modules, name, "package.json"),
|
||||
JSON.stringify({ name, version: "1.2.3" }),
|
||||
);
|
||||
}
|
||||
mkdirSync(join(modules, helper, "bin"));
|
||||
writeFileSync(join(modules, helper, "bin/scriptc-llvm-codegen"), "helper", { mode: 0o644 });
|
||||
const command = installNativeCli(f.directory, f.packageName);
|
||||
const manifest = JSON.parse(readFileSync(command + ".json", "utf8"));
|
||||
const bin = dirname(command);
|
||||
expect(resolve(bin, manifest.llvm_package)).toBe(join(modules, helper));
|
||||
expect(resolve(bin, manifest.runtime_pack)).toBe(join(modules, runtime));
|
||||
expect(manifest.runtime_packs).toEqual([
|
||||
{ target: "macos-arm64", path: manifest.runtime_pack },
|
||||
]);
|
||||
if (process.platform !== "win32")
|
||||
expect(statSync(join(modules, helper, "bin/scriptc-llvm-codegen")).mode & 0o111).toBe(0o111);
|
||||
writeFileSync(
|
||||
join(modules, runtime, "package.json"),
|
||||
JSON.stringify({ name: runtime, version: "1.2.4" }),
|
||||
);
|
||||
expect(() => installNativeCli(f.directory, f.packageName)).toThrow(
|
||||
`scriptc requires ${runtime}@1.2.3, found 1.2.4`,
|
||||
);
|
||||
rmSync(join(modules, runtime), { recursive: true });
|
||||
expect(() => installNativeCli(f.directory, f.packageName)).toThrow(
|
||||
`scriptc requires ${runtime}@1.2.3; reinstall scriptc`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("missing and mismatched packages fail without replacing an installed command", () => {
|
||||
const f = fixture();
|
||||
const command = installNativeCli(f.directory, f.packageName);
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 37, marker: "scr_runtime_abi_v37" },
|
||||
runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" },
|
||||
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_v37");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v47");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ describe("output option compatibility", () => {
|
||||
[{ emit: "llvm", strip: true }, /strip/],
|
||||
[{ emit: "obj", detachedExit: true }, /detached-exit/],
|
||||
[{ emit: "llvm", detachedExit: true }, /detached-exit/],
|
||||
[{ emit: "asm", cpuDispatch: true }, /cpu-dispatch/],
|
||||
[{ emit: "obj", profileGenerate: "/tmp/%p.profraw" }, /profile-generate/],
|
||||
[{ emit: "llvm", profileUse: "/tmp/a.profdata" }, /profile-use/],
|
||||
[{ profileGenerate: "/tmp/%p.profraw", profileUse: "/tmp/a.profdata" }, /cannot be combined/],
|
||||
|
||||
@@ -63,8 +63,16 @@ test.skipIf(process.platform !== "linux")(
|
||||
expect(merge.stdout).toBe(`${merged}\n`);
|
||||
|
||||
const optimized = join(dir, "optimized");
|
||||
await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
const fresh = await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
expect((await execFileAsync(optimized, ["1000"])).stdout).toBe("total 167499\n");
|
||||
// The build reports how the profile matched the program's functions.
|
||||
expect(fresh.stderr).toMatch(
|
||||
/^scriptc: profile .*: [1-9]\d* functions annotated, 0 mismatched, \d+ without a record\n$/,
|
||||
);
|
||||
expect(fresh.stderr).toContain(`scriptc: profile ${merged}: `);
|
||||
// A build from the cache reports the same match.
|
||||
const again = await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
expect(again.stderr).toBe(fresh.stderr);
|
||||
|
||||
// A sparse profile leaves out the runtime functions the runs never
|
||||
// reached.
|
||||
@@ -77,10 +85,15 @@ test.skipIf(process.platform !== "linux")(
|
||||
await scriptc(["build", entry, "--profile-use", sparse, "-o", optimized], env);
|
||||
expect((await execFileAsync(optimized, ["1000"])).stdout).toBe("total 167499\n");
|
||||
|
||||
// A profile from an older version of the program still builds it.
|
||||
// A profile from an older version of the program still builds it. The
|
||||
// edited function no longer matches its counts, and the build warns.
|
||||
await writeFile(entry, PROGRAM + 'console.log("edited");\n');
|
||||
await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
const stale = await scriptc(["build", entry, "--profile-use", merged, "-o", optimized], env);
|
||||
expect((await execFileAsync(optimized, ["1000"])).stdout).toBe("total 167499\nedited\n");
|
||||
expect(stale.stderr).toMatch(/ [1-9]\d* mismatched, /);
|
||||
expect(stale.stderr).toMatch(
|
||||
/\nscriptc: warning: [1-9]\d* of the \d+ functions .* describes \([\d.]+%\) changed since it was collected and are optimized without their counts; collect the profile again\n$/,
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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"');
|
||||
// Only --sanitize builds mark the functions for ASan instrumentation.
|
||||
expect(baseline).not.toContain("sanitize_address");
|
||||
expect(await build(["--target-cpu=baseline"])).toBe(baseline);
|
||||
const v3 = await build(["--target-cpu=x86-64-v3"]);
|
||||
expect(v3).toMatch(/^attributes #0 = \{ "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);
|
||||
@@ -0,0 +1,47 @@
|
||||
/// <reference path="./assert.d.ts" />
|
||||
|
||||
/* node:assert/strict — the same surface with the loose names bound to the
|
||||
* strict comparisons (equal IS strictEqual, ...), exactly Node's aliasing;
|
||||
* the module object is callable like assert's. */
|
||||
declare module "assert/strict" {
|
||||
function strict(value: unknown, message?: string): void;
|
||||
namespace strict {
|
||||
function ok(value: unknown, message?: string): void;
|
||||
function equal(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function deepEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notDeepEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function strictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function deepStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function fail(message?: string): never;
|
||||
function throws(fn: () => unknown, expected?: unknown, message?: string): void;
|
||||
function doesNotThrow(fn: () => unknown, expected?: unknown, message?: string): void;
|
||||
function rejects(
|
||||
fn: (() => Promise<unknown>) | Promise<unknown>,
|
||||
expected?: unknown,
|
||||
message?: string,
|
||||
): Promise<void>;
|
||||
function doesNotReject(
|
||||
fn: (() => Promise<unknown>) | Promise<unknown>,
|
||||
expected?: unknown,
|
||||
message?: string,
|
||||
): Promise<void>;
|
||||
function match(value: string, regExp: RegExp, message?: string): void;
|
||||
function doesNotMatch(value: string, regExp: RegExp, message?: string): void;
|
||||
function ifError(value: unknown): void;
|
||||
const strict: typeof import("assert/strict");
|
||||
type AssertOptions = import("assert").AssertOptions;
|
||||
type Assert = import("assert").Assert;
|
||||
type AssertStrict = import("assert").AssertStrict;
|
||||
const Assert: typeof import("assert").Assert;
|
||||
}
|
||||
export = strict;
|
||||
}
|
||||
|
||||
declare module "node:assert/strict" {
|
||||
import strict = require("assert/strict");
|
||||
export = strict;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/// <reference path="./assert-strict.d.ts" />
|
||||
|
||||
declare module "assert" {
|
||||
function assert(value: unknown, message?: string): void;
|
||||
namespace assert {
|
||||
interface AssertionErrorOptions {
|
||||
message?: string | undefined;
|
||||
actual?: unknown;
|
||||
expected?: unknown;
|
||||
operator?: string | undefined;
|
||||
stackStartFn?: Function | undefined;
|
||||
diff?: "simple" | "full" | undefined;
|
||||
}
|
||||
class AssertionError extends Error {
|
||||
constructor(options: AssertionErrorOptions);
|
||||
actual: unknown;
|
||||
expected: unknown;
|
||||
generatedMessage: boolean;
|
||||
code: "ERR_ASSERTION";
|
||||
operator: string;
|
||||
}
|
||||
function ok(value: unknown, message?: string): void;
|
||||
function strictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function deepStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function equal(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function deepEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function notDeepEqual(actual: unknown, expected: unknown, message?: string): void;
|
||||
function fail(message?: string): never;
|
||||
function throws(fn: () => unknown, expected?: unknown, message?: string): void;
|
||||
function doesNotThrow(fn: () => unknown, expected?: unknown, message?: string): void;
|
||||
function rejects(
|
||||
fn: (() => Promise<unknown>) | Promise<unknown>,
|
||||
expected?: unknown,
|
||||
message?: string,
|
||||
): Promise<void>;
|
||||
function doesNotReject(
|
||||
fn: (() => Promise<unknown>) | Promise<unknown>,
|
||||
expected?: unknown,
|
||||
message?: string,
|
||||
): Promise<void>;
|
||||
function match(value: string, regExp: RegExp, message?: string): void;
|
||||
function doesNotMatch(value: string, regExp: RegExp, message?: string): void;
|
||||
function ifError(value: unknown): void;
|
||||
const strict: typeof import("assert/strict");
|
||||
interface AssertOptions {
|
||||
diff?: "simple" | "full" | undefined;
|
||||
strict?: boolean | undefined;
|
||||
skipPrototype?: boolean | undefined;
|
||||
}
|
||||
type AssertMethodNames =
|
||||
| "deepEqual"
|
||||
| "deepStrictEqual"
|
||||
| "doesNotMatch"
|
||||
| "doesNotReject"
|
||||
| "doesNotThrow"
|
||||
| "equal"
|
||||
| "fail"
|
||||
| "ifError"
|
||||
| "match"
|
||||
| "notDeepEqual"
|
||||
| "notDeepStrictEqual"
|
||||
| "notEqual"
|
||||
| "notStrictEqual"
|
||||
| "ok"
|
||||
| "partialDeepStrictEqual"
|
||||
| "rejects"
|
||||
| "strictEqual"
|
||||
| "throws";
|
||||
interface Assert extends Pick<typeof assert, AssertMethodNames> {}
|
||||
interface AssertStrict extends Pick<typeof strict, AssertMethodNames> {}
|
||||
const Assert: {
|
||||
new (options?: AssertOptions & { strict?: true | undefined }): AssertStrict;
|
||||
new (options: AssertOptions): Assert;
|
||||
};
|
||||
}
|
||||
export = assert;
|
||||
}
|
||||
|
||||
declare module "node:assert" {
|
||||
import assert = require("assert");
|
||||
export = assert;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/* node:async_hooks — declared surface without lowering: async-hook
|
||||
* instrumentation is guarded behind env flags in harness code; a reached
|
||||
* use fences at its site. */
|
||||
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. 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(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;
|
||||
enterWith(store: T): void;
|
||||
disable(): void;
|
||||
}
|
||||
export interface AsyncHook {
|
||||
enable(): AsyncHook;
|
||||
disable(): AsyncHook;
|
||||
}
|
||||
export function createHook(callbacks: {
|
||||
init?: (asyncId: number, type: string, triggerAsyncId: number, resource: unknown) => void;
|
||||
before?: (asyncId: number) => void;
|
||||
after?: (asyncId: number) => void;
|
||||
destroy?: (asyncId: number) => void;
|
||||
}): AsyncHook;
|
||||
export function executionAsyncId(): number;
|
||||
export function triggerAsyncId(): number;
|
||||
}
|
||||
|
||||
declare module "node:async_hooks" {
|
||||
export * from "async_hooks";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* node:buffer — the module spelling of the Buffer/atob/btoa globals.
|
||||
* Buffer re-exports the GLOBAL declaration, so an imported binding
|
||||
* resolves to the same symbol the lowerings already answer for (and
|
||||
* third-party .d.ts files that `import { Buffer } from "node:buffer"` —
|
||||
* file-entry-cache in a typed-JS source graph — typecheck). */
|
||||
declare module "buffer" {
|
||||
export function isAscii(input: ArrayBuffer | ArrayBufferView): boolean;
|
||||
export function isUtf8(input: ArrayBuffer | ArrayBufferView): boolean;
|
||||
export function transcode(source: Uint8Array, fromEncoding: string, toEncoding: string): Buffer;
|
||||
export function atob(data: string): string;
|
||||
export function btoa(data: string): string;
|
||||
export const kMaxLength: number;
|
||||
export const kStringMaxLength: number;
|
||||
export const Buffer: BufferConstructor;
|
||||
export type Blob = globalThis.Blob;
|
||||
export type File = globalThis.File;
|
||||
export const Blob: typeof globalThis.Blob;
|
||||
export const File: typeof globalThis.File;
|
||||
}
|
||||
|
||||
declare module "node:buffer" {
|
||||
export * from "buffer";
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user