mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Merge perf/shape-overlay-core
This commit is contained in:
@@ -346,6 +346,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:
|
||||
@@ -950,6 +981,7 @@ jobs:
|
||||
tests_macos,
|
||||
native_linux,
|
||||
effects,
|
||||
three_nodes_san,
|
||||
bootstrap_seed,
|
||||
bootstrap,
|
||||
linux_host_clang,
|
||||
@@ -982,6 +1014,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
|
||||
+117
-17
@@ -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,11 +101,11 @@ jobs:
|
||||
if (receiptRun) appendFileSync(process.env.GITHUB_OUTPUT, `receipt_run=${receiptRun}\n`);
|
||||
NODE
|
||||
|
||||
# Collects the native compiler's profile on this commit
|
||||
# (scripts/refresh-cli-profile.mjs), so every platform's CLI build optimizes
|
||||
# with a profile of the code it compiles. The committed profile falls behind
|
||||
# main within a day or two. If this job fails, the CLI builds use the
|
||||
# committed profile.
|
||||
# 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
|
||||
@@ -138,11 +139,17 @@ jobs:
|
||||
- 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"
|
||||
--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
|
||||
path: |
|
||||
${{ runner.temp }}/cli-profile/cli.profdata.gz
|
||||
${{ runner.temp }}/cli-profile/cli.layout.gz
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -173,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
|
||||
@@ -180,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
|
||||
@@ -195,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
|
||||
@@ -204,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.
|
||||
@@ -240,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:
|
||||
@@ -278,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
|
||||
@@ -300,8 +342,8 @@ jobs:
|
||||
with:
|
||||
name: cli-profile
|
||||
path: ${{ runner.temp }}/cli-profile
|
||||
# Without the artifact, SCRIPTC_NATIVE_PROFILE_USE stays unset and
|
||||
# build-native-cli.mts uses the committed profile.
|
||||
# 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
|
||||
@@ -311,6 +353,11 @@ jobs:
|
||||
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
|
||||
@@ -394,6 +441,8 @@ jobs:
|
||||
contents: read
|
||||
actions: read
|
||||
id-token: write
|
||||
outputs:
|
||||
uploads: ${{ steps.publish.outputs.uploads }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -413,7 +462,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
|
||||
@@ -422,6 +471,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
|
||||
@@ -471,6 +521,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Publish to npm
|
||||
id: publish
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -490,10 +541,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
|
||||
@@ -510,6 +565,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]}
|
||||
@@ -533,7 +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.
|
||||
# upload must not block the remaining packages; the npm-available
|
||||
# job waits for it.
|
||||
publish_dir packages/runtime
|
||||
publish_dir packages/runtime-darwin-arm64
|
||||
publish_dir packages/llvm-darwin-arm64
|
||||
@@ -561,23 +618,66 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+36
-2
@@ -6,6 +6,42 @@ 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
|
||||
@@ -29,8 +65,6 @@ All notable changes to scriptc will be documented in this file.
|
||||
- Split CommonJS `require` bindings, `__`-prefixed exports, string-key destructuring of `any` values, and record key order for values JavaScript built.
|
||||
- **npm packages:** zod 4 and `@hono/zod-validator` values are projected at the declaration boundary.
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -18,6 +18,8 @@ The release job builds each pinned LLVM helper, precompiled runtime pack, and na
|
||||
|
||||
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
|
||||
`release.yml` and the `Release` environment. A missing configuration fails
|
||||
|
||||
@@ -16,6 +16,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `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 |
|
||||
@@ -30,7 +31,9 @@ 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 |
|
||||
|
||||
@@ -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/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);
|
||||
@@ -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.
|
||||
@@ -138,11 +163,14 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
<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>
|
||||
@@ -234,6 +262,25 @@ $ 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:
|
||||
@@ -271,6 +318,22 @@ An executable built for a level runs only on CPUs that support that level. On an
|
||||
|
||||
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.
|
||||
|
||||
@@ -31,22 +31,25 @@
|
||||
`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, Ed25519, and X25519 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, 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 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`, `x448`, 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.
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
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.
|
||||
The first `pipeline` stage may arrive as an untyped value that is a native stream, a `Cipheriv` or `Decipheriv` value, or an iterable: a string, an array, a generator object, or any value with a `Symbol.asyncIterator` or `Symbol.iterator` method, which the pipeline pumps into the next stage as Node does.
|
||||
Later `pipeline` stages and `for await` sources that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
|
||||
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 +14,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).
|
||||
|
||||
@@ -2,12 +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` when the lookup is inside a `try` block.
|
||||
There, a `createRequire` require call with a computed specifier, and a `require.resolve` or `require.resolve.paths` call with a computed request, evaluate their arguments and then throw an error.
|
||||
Its `require.resolve` read as a value is a function that throws when called.
|
||||
Coverage lists these as deferred to runtime.
|
||||
The package's `catch` clause then runs as it does in Node.js when the addon is missing.
|
||||
Outside a `try` block, the same uses keep the package refused.
|
||||
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.
|
||||
|
||||
@@ -25,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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25806,8 +25806,12 @@
|
||||
"anchor": "dep0044-utilisarray",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-applicable",
|
||||
"evidence": "compiler-chapter-policy:deprecations"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.isArray",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -26094,8 +26098,12 @@
|
||||
"anchor": "dep0060-util_extend",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-applicable",
|
||||
"evidence": "compiler-chapter-policy:deprecations"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util._extend",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -32763,6 +32771,8 @@
|
||||
"tests/corpus/6702-ecdh-errors.js",
|
||||
"tests/corpus/6708-dh-errors.js",
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7711-util-throttle.cjs",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/caught-system-error-fields.ts",
|
||||
"tests/corpus/error-stack-frames.ts",
|
||||
@@ -36688,8 +36698,11 @@
|
||||
"anchor": "err_invalid_mime_syntax",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:errors.ERR_INVALID_MIME_SYNTAX",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -38826,8 +38839,11 @@
|
||||
"anchor": "err_throttled",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:errors.ERR_THROTTLED",
|
||||
"tests": [
|
||||
"tests/corpus/7711-util-throttle.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101568,6 +101584,25 @@
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts",
|
||||
"tests/corpus/5884-inspect-layout-options.ts",
|
||||
"tests/corpus/6428-promisify-callback-functions.ts",
|
||||
"tests/corpus/7520-util-format-runtime.ts",
|
||||
"tests/corpus/7521-util-format-runtime.cjs",
|
||||
"tests/corpus/7522-promisify-untyped.cjs",
|
||||
"tests/corpus/7523-promisify-untyped.ts",
|
||||
"tests/corpus/7524-inspect-custom.ts",
|
||||
"tests/corpus/7525-inspect-custom.cjs",
|
||||
"tests/corpus/7526-inspect-options-runtime.ts",
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts",
|
||||
"tests/corpus/7702-util-signal-exit-code.cjs",
|
||||
"tests/corpus/7703-util-aborted.cjs",
|
||||
"tests/corpus/7704-util-aborted.ts",
|
||||
"tests/corpus/7705-util-diff.cjs",
|
||||
"tests/corpus/7706-util-diff.ts",
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts",
|
||||
"tests/corpus/7710-util-debounce.cjs",
|
||||
"tests/corpus/7711-util-throttle.cjs",
|
||||
"tests/corpus/7712-util-debounce-throttle.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js",
|
||||
"tests/corpus/node24-deep-equality-properties.js",
|
||||
"tests/corpus/node24-deep-equality.cjs",
|
||||
@@ -101654,8 +101689,12 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util._extend",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101677,8 +101716,12 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.isArray",
|
||||
"tests": [
|
||||
"tests/corpus/7700-util-deprecated-helpers.cjs",
|
||||
"tests/corpus/7701-util-deprecated-helpers.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101700,8 +101743,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.util.MIMEType",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101723,8 +101770,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mime.toString",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101746,8 +101797,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mime.toJSON",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101769,8 +101824,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.MIMEType.parse",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101884,8 +101943,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.util.MIMEParams",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101907,8 +101970,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.delete",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101930,8 +101997,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.entries",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101953,8 +102024,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.get",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101976,8 +102051,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.has",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -101999,8 +102078,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.keys",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102022,8 +102105,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.set",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102045,8 +102132,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams.values",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102068,8 +102159,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.mimeParams",
|
||||
"tests": [
|
||||
"tests/corpus/7707-util-mime-type.cjs",
|
||||
"tests/corpus/7708-util-mime-type.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102468,8 +102563,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.convertProcessSignalToExitCode",
|
||||
"tests": [
|
||||
"tests/corpus/7702-util-signal-exit-code.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102603,8 +102701,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.debounce",
|
||||
"tests": [
|
||||
"tests/corpus/7710-util-debounce.cjs",
|
||||
"tests/corpus/7712-util-debounce-throttle.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102626,8 +102728,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.throttle",
|
||||
"tests": [
|
||||
"tests/corpus/7711-util-throttle.cjs",
|
||||
"tests/corpus/7712-util-debounce-throttle.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102649,8 +102755,12 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.diff",
|
||||
"tests": [
|
||||
"tests/corpus/7705-util-diff.cjs",
|
||||
"tests/corpus/7706-util-diff.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102677,7 +102787,9 @@
|
||||
"tests": [
|
||||
"tests/corpus/1636-util-format.ts",
|
||||
"tests/corpus/2440-console-inspect-args.ts",
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts"
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts",
|
||||
"tests/corpus/7520-util-format-runtime.ts",
|
||||
"tests/corpus/7521-util-format-runtime.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -102705,7 +102817,9 @@
|
||||
"tests": [
|
||||
"tests/corpus/1636-util-format.ts",
|
||||
"tests/corpus/2440-console-inspect-args.ts",
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts"
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts",
|
||||
"tests/corpus/7520-util-format-runtime.ts",
|
||||
"tests/corpus/7521-util-format-runtime.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -102896,7 +103010,8 @@
|
||||
"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/5884-inspect-layout-options.ts",
|
||||
"tests/corpus/7526-inspect-options-runtime.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -102919,8 +103034,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.util.inspect",
|
||||
"tests": [
|
||||
"tests/corpus/7526-inspect-options-runtime.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -102942,8 +103060,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:util.custom",
|
||||
"tests": [
|
||||
"tests/corpus/7524-inspect-custom.ts",
|
||||
"tests/corpus/7525-inspect-custom.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -103260,6 +103382,8 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
@@ -103475,8 +103599,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:util"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.util.aborted",
|
||||
"tests": [
|
||||
"tests/corpus/7703-util-aborted.cjs",
|
||||
"tests/corpus/7704-util-aborted.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -187,6 +187,13 @@
|
||||
"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"],
|
||||
@@ -253,9 +260,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" },
|
||||
@@ -265,6 +274,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"] },
|
||||
@@ -409,7 +422,7 @@
|
||||
{ "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"] },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,7 +37,7 @@ Repeating `--output` divides the program into that many partitions, which are co
|
||||
|
||||
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.
@@ -173,6 +173,23 @@ void limitProfileImports() {
|
||||
(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
|
||||
@@ -733,6 +750,7 @@ compileOptimizedPartitions(const EmitOptions &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));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-arm64",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for darwin-arm64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-x64",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for darwin-x64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-gnu",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-arm64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-musl",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-arm64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-gnu",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-x64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-musl",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for linux-x64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-win32-x64-msvc",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "Native scriptc compiler for win32-x64-msvc",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scriptc",
|
||||
"version": "0.4.4",
|
||||
"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]);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ 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
|
||||
@@ -225,6 +226,7 @@ async function coverageFastPath(
|
||||
values.strip ||
|
||||
values["detached-exit"] ||
|
||||
values["target-cpu"] !== undefined ||
|
||||
values["cpu-dispatch"] !== undefined ||
|
||||
values.lib ||
|
||||
values.profile !== undefined ||
|
||||
values.ffi !== undefined ||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -53,6 +53,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 +94,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"),
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 46, marker: "scr_runtime_abi_v46" },
|
||||
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_v43");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v46");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -50,9 +50,11 @@ test.runIf(process.platform === "linux" && process.arch === "x64")(
|
||||
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 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
|
||||
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);
|
||||
|
||||
+182
-14
@@ -1718,6 +1718,16 @@ declare module "node:fs" {
|
||||
): void;
|
||||
export function rmdirSync(path: string | URL): void;
|
||||
export function readdirSync(path: string | URL): string[];
|
||||
/* The recursive form: every entry below the path, relative to it. */
|
||||
export function readdirSync(
|
||||
path: string | URL,
|
||||
options: {
|
||||
recursive?: boolean;
|
||||
withFileTypes?: false;
|
||||
encoding?: "utf8" | "utf-8";
|
||||
[option: string]: unknown;
|
||||
},
|
||||
): string[];
|
||||
/* The withFileTypes form: Dirent rows (name + parentPath + the type
|
||||
* probes — the honest subset of Node's Dirent surface). The type is
|
||||
* interned explicitly in type-mapper.ts (the record's hidden %dtype field
|
||||
@@ -2914,6 +2924,8 @@ declare module "crypto" {
|
||||
export interface AsymmetricKeyExportOptions<T extends KeyFormat> {
|
||||
type: "pkcs1" | "spki" | "pkcs8" | "sec1";
|
||||
format: T;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | Buffer | undefined;
|
||||
}
|
||||
export interface AsymmetricKeyDetails {
|
||||
modulusLength?: number;
|
||||
@@ -2938,6 +2950,7 @@ declare module "crypto" {
|
||||
format?: "pem" | "der" | undefined;
|
||||
type?: "pkcs1" | "pkcs8" | "sec1" | undefined;
|
||||
encoding?: string | undefined;
|
||||
passphrase?: string | Buffer | undefined;
|
||||
}
|
||||
export interface PublicKeyInput {
|
||||
key: string | Buffer;
|
||||
@@ -2974,12 +2987,22 @@ declare module "crypto" {
|
||||
modulusLength: number;
|
||||
publicExponent?: number | undefined;
|
||||
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs1" | "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export interface ECKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
namedCurve: string;
|
||||
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "sec1" | "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "rsa",
|
||||
@@ -3007,7 +3030,12 @@ declare module "crypto" {
|
||||
): KeyPairKeyObjectResult;
|
||||
export interface ED25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
publicKeyEncoding: { type: "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "ed25519",
|
||||
@@ -3020,7 +3048,12 @@ declare module "crypto" {
|
||||
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
|
||||
export interface X25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
publicKeyEncoding: { type: "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "x25519",
|
||||
@@ -3938,6 +3971,85 @@ declare module "util" {
|
||||
): string;
|
||||
export function parseEnv(content: string): { [key: string]: string | undefined };
|
||||
export function inherits(constructor: unknown, superConstructor: unknown): void;
|
||||
export class MIMEType {
|
||||
constructor(input: string | { toString: () => string });
|
||||
static parse(input: string | { toString: () => string }): MIMEType | null;
|
||||
type: string;
|
||||
subtype: string;
|
||||
readonly essence: string;
|
||||
readonly params: MIMEParams;
|
||||
toString(): string;
|
||||
toJSON(): string;
|
||||
}
|
||||
export class MIMEParams {
|
||||
delete(name: string): void;
|
||||
entries(): IterableIterator<[name: string, value: string]>;
|
||||
get(name: string): string | null;
|
||||
has(name: string): boolean;
|
||||
keys(): IterableIterator<string>;
|
||||
set(name: string, value: string): void;
|
||||
values(): IterableIterator<string>;
|
||||
toString(): string;
|
||||
toJSON(): string;
|
||||
[Symbol.iterator](): IterableIterator<[name: string, value: string]>;
|
||||
}
|
||||
export type DiffEntry = [operation: -1 | 0 | 1, value: string];
|
||||
export function diff(
|
||||
actual: string | readonly string[],
|
||||
expected: string | readonly string[],
|
||||
): DiffEntry[];
|
||||
export interface DebounceOptions {
|
||||
leading?: boolean;
|
||||
rejectOnCancel?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
/** Every call answers a promise of the result of the call it waits for. */
|
||||
export interface DebouncedFunction<T extends (...args: any[]) => any> {
|
||||
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
|
||||
cancel(reason?: unknown): void;
|
||||
flush(): void;
|
||||
ref(): this;
|
||||
unref(): this;
|
||||
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
|
||||
readonly pendingCount: number;
|
||||
}
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
wait: number,
|
||||
options?: DebounceOptions,
|
||||
): DebouncedFunction<T>;
|
||||
export interface ThrottleOptions {
|
||||
concurrency?: number;
|
||||
maxPending?: number;
|
||||
overflow?: "queue" | "drop";
|
||||
signal?: AbortSignal;
|
||||
strict?: boolean;
|
||||
}
|
||||
export interface ThrottledFunction<T extends (...args: any[]) => any> {
|
||||
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
|
||||
cancel(reason?: unknown): void;
|
||||
hasImmediateCapacity(): boolean;
|
||||
ref(): this;
|
||||
unref(): this;
|
||||
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
|
||||
readonly pendingCount: number;
|
||||
readonly activeCount: number;
|
||||
}
|
||||
export function throttle<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
limit: number,
|
||||
interval: number,
|
||||
options?: ThrottleOptions,
|
||||
): ThrottledFunction<T>;
|
||||
/** Resolves with the abort event once `signal` aborts, or with undefined
|
||||
* when it already has. */
|
||||
export function aborted(signal: AbortSignal, resource: any): Promise<unknown>;
|
||||
/** 128 plus the number of the signal, the exit code of a process it terminates. */
|
||||
export function convertProcessSignalToExitCode(signalCode: string): number;
|
||||
/** @deprecated Since v4.0.0 - Use `Array.isArray()` instead. */
|
||||
export function isArray(object: unknown): object is unknown[];
|
||||
/** @deprecated Since v6.0.0 - Use `Object.assign()` instead. Answers target itself. */
|
||||
export function _extend<T extends object>(target: T, source: object | null | undefined): T;
|
||||
export interface DeprecateOptions {
|
||||
modifyPrototype?: boolean;
|
||||
}
|
||||
@@ -3995,7 +4107,13 @@ declare module "util" {
|
||||
export type TextDecoder = globalThis.TextDecoder;
|
||||
export var TextEncoder: typeof globalThis.TextEncoder;
|
||||
export var TextDecoder: typeof globalThis.TextDecoder;
|
||||
/* stream.pipeline's overload comes first: its __promisify__ member keeps
|
||||
/* An untyped function: an `any` value, or a function whose parameters
|
||||
* are untyped (JavaScript's `(value, callback) => ...`), the only
|
||||
* parameters that accept this object type. The promisified function is
|
||||
* untyped too. It comes first because such functions match every other
|
||||
* overload. */
|
||||
export function promisify(fn: (...args: { readonly "scriptc:untyped": true }[]) => unknown): any;
|
||||
/* stream.pipeline's overload comes next: its __promisify__ member keeps
|
||||
* every other target out of it. */
|
||||
export function promisify(
|
||||
fn: typeof import("node:stream").pipeline,
|
||||
@@ -4116,15 +4234,56 @@ declare module "util" {
|
||||
): (address: string, port: number) => Promise<{ hostname: string; service: string }>;
|
||||
/* Declared surface without full lowering (the type-level tolerance
|
||||
* story: guarded/diagnostic-path code must TYPECHECK; a reached use
|
||||
* without a lowering fences at its site). inspect's options carry the
|
||||
* documented knobs through an index signature. */
|
||||
* without a lowering fences at its site). */
|
||||
export interface InspectOptions {
|
||||
showHidden?: boolean | undefined;
|
||||
depth?: number | null | undefined;
|
||||
colors?: boolean | undefined;
|
||||
customInspect?: boolean | undefined;
|
||||
showProxy?: boolean | undefined;
|
||||
maxArrayLength?: number | null | undefined;
|
||||
maxStringLength?: number | null | undefined;
|
||||
breakLength?: number | undefined;
|
||||
compact?: boolean | number | undefined;
|
||||
sorted?: boolean | ((a: string, b: string) => number) | undefined;
|
||||
getters?: "get" | "set" | boolean | undefined;
|
||||
numericSeparator?: boolean | undefined;
|
||||
}
|
||||
export type Style =
|
||||
| "special"
|
||||
| "number"
|
||||
| "bigint"
|
||||
| "boolean"
|
||||
| "undefined"
|
||||
| "null"
|
||||
| "string"
|
||||
| "symbol"
|
||||
| "date"
|
||||
| "regexp"
|
||||
| "module";
|
||||
export interface InspectOptionsStylized extends InspectOptions {
|
||||
stylize(text: string, styleType: Style): string;
|
||||
}
|
||||
export type CustomInspectFunction = (
|
||||
depth: number,
|
||||
options: InspectOptionsStylized,
|
||||
inspect: typeof import("node:util").inspect,
|
||||
) => any;
|
||||
export function inspect(
|
||||
object: unknown,
|
||||
options?: { depth?: number | null; colors?: boolean; [k: string]: unknown },
|
||||
showHidden?: boolean,
|
||||
depth?: number | null,
|
||||
color?: boolean,
|
||||
): string;
|
||||
export function inspect(object: unknown, options?: InspectOptions): string;
|
||||
export namespace inspect {
|
||||
/* The key of an object's custom inspect function:
|
||||
* Symbol.for('nodejs.util.inspect.custom'). */
|
||||
const custom: unique symbol;
|
||||
}
|
||||
export function format(format?: unknown, ...args: unknown[]): string;
|
||||
export function formatWithOptions(
|
||||
inspectOptions: { depth?: number | null; colors?: boolean; [k: string]: unknown },
|
||||
inspectOptions: InspectOptions,
|
||||
format?: unknown,
|
||||
...args: unknown[]
|
||||
): string;
|
||||
@@ -5322,10 +5481,10 @@ declare module "node:zlib" {
|
||||
* with the real port discoverable as address().port (the composed read
|
||||
* is the lowered form — bare address() has no lowering); close([cb])
|
||||
* firing once connections drain; socket write/end/destroy/pipe and the
|
||||
* data/end/close/error/connect events (on and once — listeners take at
|
||||
* most the declared parameter). write/end return void here (Node returns
|
||||
* the backpressure boolean / the socket); on/once return void (chaining
|
||||
* is fenced). connect's host is a numeric IP or "localhost" (pinned to
|
||||
* data/end/close/error/connect/drain events (on and once — listeners
|
||||
* take at most the declared parameter). write returns Node's
|
||||
* backpressure boolean; end returns void here (Node returns the socket);
|
||||
* on/once return void (chaining is fenced). connect's host is a numeric IP or "localhost" (pinned to
|
||||
* 127.0.0.1 — SEMANTICS.md); every failure is the async 'error' event,
|
||||
* and an 'error' with no listener exits 1 like an unhandled EventEmitter
|
||||
* 'error'. */
|
||||
@@ -5357,7 +5516,13 @@ declare module "net" {
|
||||
readonly bytesWritten: number;
|
||||
/* write's callback runs once the data is flushed, with null; end's
|
||||
* once the socket finishes. */
|
||||
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void;
|
||||
/* false when the bytes this write left unsent reach the 65536-byte
|
||||
* high-water mark, as in Node; 'drain' fires when they are all sent. */
|
||||
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): boolean;
|
||||
/* the bytes written and not yet sent */
|
||||
readonly writableLength: number;
|
||||
/* a write answered false and 'drain' has not fired yet */
|
||||
readonly writableNeedDrain: boolean;
|
||||
end(callback?: () => void): void;
|
||||
end(data: string | Uint8Array, callback?: () => void): void;
|
||||
destroy(): void;
|
||||
@@ -5405,6 +5570,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
@@ -5429,6 +5595,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
@@ -5452,6 +5619,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
|
||||
Binary file not shown.
@@ -56,7 +56,9 @@ void scr_collect_cycles_phase(void);
|
||||
* frontend's syntax trees and program state refer to each other, so
|
||||
* reference counting frees almost none of them when the frontend is
|
||||
* disposed, and the collector's schedule alone may not run a full pass
|
||||
* before the build ends. */
|
||||
* before the build ends. The sweep then returns the freed pages to the
|
||||
* system (scr_heap_trim), so code generation does not grow on top of the
|
||||
* frontend's heap. */
|
||||
void scriptc_native_collect_cycles(void) {
|
||||
scr_collect_cycles_phase();
|
||||
}
|
||||
@@ -80,9 +82,30 @@ void scriptc_native_unlock(void) {}
|
||||
#include <sys/file.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
int32_t scriptc_native_fork(void) { return (int32_t)fork(); }
|
||||
#if defined(__linux__)
|
||||
/* LLVM's profile runtime, linked into instrumented builds of the compiler
|
||||
* (SCRIPTC_NATIVE_PROFILE_GENERATE); other builds leave these null. A worker
|
||||
* starts with zeroed counters and writes its profile before _exit, which
|
||||
* skips the runtime's atexit writer, so the profile counts the worker's work
|
||||
* once. */
|
||||
extern void __llvm_profile_reset_counters(void) __attribute__((weak));
|
||||
extern int __llvm_profile_write_file(void) __attribute__((weak));
|
||||
#endif
|
||||
|
||||
void scriptc_native_exit(int32_t code) { _exit(code); }
|
||||
int32_t scriptc_native_fork(void) {
|
||||
pid_t pid = fork();
|
||||
#if defined(__linux__)
|
||||
if (pid == 0 && __llvm_profile_reset_counters != NULL) __llvm_profile_reset_counters();
|
||||
#endif
|
||||
return (int32_t)pid;
|
||||
}
|
||||
|
||||
void scriptc_native_exit(int32_t code) {
|
||||
#if defined(__linux__)
|
||||
if (__llvm_profile_write_file != NULL) (void)__llvm_profile_write_file();
|
||||
#endif
|
||||
_exit(code);
|
||||
}
|
||||
|
||||
/* The worker's exit status, 128 plus the signal for a worker a signal
|
||||
* ended, or -1 when waitpid fails. */
|
||||
|
||||
@@ -64,8 +64,11 @@ static int child_main(int argc, char **argv) {
|
||||
#endif
|
||||
int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0;
|
||||
int fixed = listening ? 9 : 5;
|
||||
if (argc < fixed || argc > fixed + 1 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == fixed + 1 && strcmp(argv[fixed], "--timing") != 0))
|
||||
int at = fixed, timing = 0, libraries = 0;
|
||||
if (at < argc && strcmp(argv[at], "--timing") == 0) { timing = 1; at++; }
|
||||
if (at < argc && strcmp(argv[at], "--default-libraries-from-disk") == 0) { libraries = 1; at++; }
|
||||
if (argc < fixed || at != argc || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0)
|
||||
return 71;
|
||||
const char *mode = argv[3];
|
||||
#ifndef _WIN32
|
||||
@@ -76,7 +79,8 @@ static int child_main(int argc, char **argv) {
|
||||
fputc('\n', stdout);
|
||||
fputs(argv[4], stdout);
|
||||
fputc('\n', stdout);
|
||||
fputs(argc == 6 ? "timing\n" : "plain\n", stdout);
|
||||
fputs(timing ? "timing\n" : "plain\n", stdout);
|
||||
if (libraries) fputs("libraries from disk\n", stdout);
|
||||
return ferror(stdout) ? 72 : 0;
|
||||
}
|
||||
if (strcmp(mode, "exit") == 0) return 0;
|
||||
|
||||
@@ -161,7 +161,7 @@ static wchar_t *command_line(wchar_t **arguments, size_t count) {
|
||||
}
|
||||
|
||||
static int start_process(Ts7Process *p, char **args, size_t argc) {
|
||||
wchar_t *wide[6] = {0};
|
||||
wchar_t *wide[7] = {0};
|
||||
wchar_t *line = NULL;
|
||||
HANDLE child_input = NULL, child_output = NULL, child_error = NULL;
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL;
|
||||
@@ -330,7 +330,7 @@ static void stop_process(Ts7Process *p) {
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing) {
|
||||
uint8_t timing, uint8_t libraries_from_disk) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *exe = copy_argument(executable, executable_size, "");
|
||||
if (exe == NULL) return 0;
|
||||
@@ -346,8 +346,12 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
}
|
||||
exit_registered = 1;
|
||||
}
|
||||
char *args[] = {exe, "--api", "--cwd", directory, callback_flag, timing ? "--timing" : NULL, NULL};
|
||||
int started = start_process(p, args, timing ? 6 : 5);
|
||||
char *args[8] = {exe, "--api", "--cwd", directory, callback_flag};
|
||||
size_t argc = 5;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
int started = start_process(p, args, argc);
|
||||
free(exe); free(directory); free(callback_flag);
|
||||
if (!started) { free(p); return 0; }
|
||||
p->id = next_id++;
|
||||
@@ -361,9 +365,10 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory) {
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
(void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)idle_exit; (void)memory;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)libraries_from_disk; (void)idle_exit; (void)memory;
|
||||
error_text("a shared checker server needs Unix sockets");
|
||||
return 0;
|
||||
}
|
||||
@@ -455,7 +460,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory) {
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *server = copy_argument(name, name_size, "");
|
||||
if (server == NULL) return 0;
|
||||
@@ -491,8 +497,11 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
(void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory);
|
||||
pid_t pid = -1;
|
||||
if (callback_flag != NULL) {
|
||||
char *args[] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle,
|
||||
timing ? "--timing" : NULL, NULL};
|
||||
char *args[12] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle};
|
||||
size_t argc = 9;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
pid = start_server(args, limit);
|
||||
}
|
||||
free(exe); free(dir); free(callback_flag);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" },
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing);
|
||||
uint8_t timing, uint8_t libraries_from_disk);
|
||||
/* Connects to the shared checker server `name` (`--api --listen`) in this
|
||||
* user's private server directory, starting it in its own process group
|
||||
* with `--idle-exit idle_exit` and a limit of `memory` megabytes
|
||||
@@ -25,7 +25,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory);
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory);
|
||||
int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size,
|
||||
uint32_t offset, uint32_t length);
|
||||
int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#ifndef _WIN32
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#endif
|
||||
/* macOS declares mkdtemp only with the Darwin extensions visible. */
|
||||
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
||||
#define _DARWIN_C_SOURCE 1
|
||||
#endif
|
||||
#include "ts7-process.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
@@ -21,12 +25,14 @@
|
||||
static const char *executable;
|
||||
static const char *callbacks = "readFile,fileExists";
|
||||
|
||||
static uint32_t start(const char *mode, int timing) {
|
||||
static uint32_t start_with(const char *mode, int timing, int libraries) {
|
||||
return scriptc_ts7_open((const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode),
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing);
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing, (uint8_t)libraries);
|
||||
}
|
||||
|
||||
static uint32_t start(const char *mode, int timing) { return start_with(mode, timing, 0); }
|
||||
|
||||
static void fail_message(void) {
|
||||
uint8_t message[1024];
|
||||
int32_t size = scriptc_ts7_error(message, sizeof(message));
|
||||
@@ -83,11 +89,13 @@ static void argument_cases(void) {
|
||||
"", "a b", "double\"quote", "end\\", "many\\\\\"quotes\\\\", "a;&$()`'b", "h\xc3\xa9llo \xf0\x9f\x8c\x8d",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (int timing = 0; timing <= 1; timing++) {
|
||||
for (int flags = 0; flags < 4; flags++) {
|
||||
int timing = flags & 1, libraries = flags >> 1;
|
||||
char mode[256], expected[512];
|
||||
snprintf(mode, sizeof(mode), "argv:%s", values[i]);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n", values[i], callbacks, timing ? "timing" : "plain");
|
||||
uint32_t id = start(mode, timing);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n%s", values[i], callbacks,
|
||||
timing ? "timing" : "plain", libraries ? "libraries from disk\n" : "");
|
||||
uint32_t id = start_with(mode, timing, libraries);
|
||||
assert(id != 0);
|
||||
uint8_t actual[512];
|
||||
read_exact(id, actual, strlen(expected));
|
||||
@@ -147,10 +155,10 @@ static void failure_cases(void) {
|
||||
executable = "/scriptc/does-not-exist/tsgo";
|
||||
for (int i = 0; i < 30; i++) { assert(start("echo", 0) == 0); fail_message(); }
|
||||
executable = saved;
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0, 0) == 0);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
uint32_t id = start("exit", 0);
|
||||
assert(id != 0);
|
||||
@@ -177,7 +185,7 @@ static void failure_cases(void) {
|
||||
static uint32_t attach(const char *name, const char *mode, uint32_t memory) {
|
||||
return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks),
|
||||
0, 1, memory);
|
||||
0, 0, 1, memory);
|
||||
}
|
||||
|
||||
static void expect_line(uint32_t id, const char *expected) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/compiler",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.5",
|
||||
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/runtime": "workspace:*",
|
||||
"tsc-ts": "0.0.9",
|
||||
"tsc-ts": "0.0.10",
|
||||
"typescript": "7.0.2",
|
||||
"typescript5": "npm:typescript@5.9.3"
|
||||
},
|
||||
|
||||
@@ -31,10 +31,14 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.declAdopt ? { declAdopt: true } : {}),
|
||||
...(features.typeOrigins ? { typeOrigins: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.mime ? { mime: true } : {}),
|
||||
...(features.utilTiming ? { utilTiming: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
...(features.computedRequire ? { computedRequire: true } : {}),
|
||||
events: features.processEvents,
|
||||
emitter: features.emitter,
|
||||
symbol: features.symbol,
|
||||
|
||||
@@ -166,6 +166,7 @@ export async function compilePackedLibrary(
|
||||
options.programPublicSymbols,
|
||||
stem,
|
||||
target.platform,
|
||||
{ inProcessElf: true },
|
||||
);
|
||||
} else {
|
||||
program = await emit(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { systemLibraryLinkerArgs } from "./link-plan-core.js";
|
||||
import { MACOS_ARM64_TARGET, LINUX_ARM64_GNU_TARGET, LINUX_X64_GNU_TARGET } from "./targets.js";
|
||||
|
||||
test("Linux links libm as needed and other system libraries as given", () => {
|
||||
for (const target of [LINUX_X64_GNU_TARGET, LINUX_ARM64_GNU_TARGET]) {
|
||||
expect(systemLibraryLinkerArgs(target, ["m"])).toEqual([
|
||||
"-Wl,--as-needed",
|
||||
"-lm",
|
||||
"-Wl,--no-as-needed",
|
||||
]);
|
||||
expect(systemLibraryLinkerArgs(target, ["z", "m"])).toEqual([
|
||||
"-lz",
|
||||
"-Wl,--as-needed",
|
||||
"-lm",
|
||||
"-Wl,--no-as-needed",
|
||||
]);
|
||||
}
|
||||
expect(systemLibraryLinkerArgs(MACOS_ARM64_TARGET, ["System"])).toEqual(["-lSystem"]);
|
||||
});
|
||||
@@ -70,3 +70,19 @@ export function executableLinkInputs(options: {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/** The linker arguments for an executable's system libraries. On Linux, libm
|
||||
* links as needed: the runtime defines the libm functions most programs
|
||||
* reach (scr_glibc_math.c) and Math's transcendentals (scr_ieee754.c), so a
|
||||
* program that calls no other libm function, such as pow, starts without
|
||||
* loading libm.so. */
|
||||
export function systemLibraryLinkerArgs(
|
||||
target: Pick<NativeTargetSpec, "platform">,
|
||||
names: readonly string[],
|
||||
): string[] {
|
||||
return names.flatMap((name) =>
|
||||
name === "m" && target.platform === "linux"
|
||||
? ["-Wl,--as-needed", "-lm", "-Wl,--no-as-needed"]
|
||||
: [`-l${name}`],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { lldLinkerArgs } from "./lld.js";
|
||||
import { systemLibraryLinkerArgs } from "./link-plan-core.js";
|
||||
import {
|
||||
createDarwinDebugSymbols,
|
||||
installDarwinDebugSymbols,
|
||||
@@ -248,7 +249,7 @@ export async function linkNativeExecutable(
|
||||
...lld,
|
||||
...plan.driverFlags,
|
||||
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
|
||||
...plan.systemLibraries.map((name) => `-l${name}`),
|
||||
...systemLibraryLinkerArgs(plan.target, plan.systemLibraries),
|
||||
"-o",
|
||||
privateOut,
|
||||
];
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* one directly or through a binding read from it. The choice never changes
|
||||
* behavior. */
|
||||
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const APPENDING_METHODS: ReadonlySet<string> = new Set([
|
||||
@@ -120,3 +120,36 @@ export function findAppendedArrayLiterals(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `const xs = []` directly followed by a capacity hint for `xs` (an
|
||||
* `arraySetLength` with `reserve`) whose length reads only literals and
|
||||
* other locals: the literal can be constructed with that capacity, and the
|
||||
* hint then has nothing left to do. */
|
||||
export function reservedArrayLength(
|
||||
stmts: readonly IrStmt[],
|
||||
i: number,
|
||||
): { literal: IrExpr; hint: IrStmt & { kind: "arraySetLength" } } | null {
|
||||
const decl = stmts[i];
|
||||
const next = stmts[i + 1];
|
||||
if (decl?.kind !== "varDecl" || decl.init === null || !isEmptyLiteral(decl.init)) return null;
|
||||
if (next?.kind !== "arraySetLength" || next.reserve !== true) return null;
|
||||
if (next.arr.kind !== "varRef" || next.arr.localId !== decl.localId) return null;
|
||||
return plainLength(next.length, decl.localId) ? { literal: decl.init, hint: next } : null;
|
||||
}
|
||||
|
||||
function plainLength(e: IrExpr, array: string): boolean {
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
return true;
|
||||
case "varRef":
|
||||
return e.localId !== array;
|
||||
case "bin":
|
||||
return plainLength(e.left, array) && plainLength(e.right, array);
|
||||
case "ternary":
|
||||
return (
|
||||
plainLength(e.cond, array) && plainLength(e.then, array) && plainLength(e.else_, array)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,25 @@ import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
* receiver slots loop-invariant. Untagged accesses and calls still alias
|
||||
* every category.
|
||||
*
|
||||
* Header loads are also `!invariant.load`: no store or call between the
|
||||
* construction of a view and its release changes them. Without it, a data
|
||||
* pointer load ahead of a bounds branch is sunk into the in-bounds block,
|
||||
* and LICM keeps a conditionally executed load inside the loop. */
|
||||
* Header loads that only bound or address an access to the view are also
|
||||
* `!invariant.load`: no store or call between the construction of a view
|
||||
* and its release changes them, and their values reach only that access,
|
||||
* which runs while the view is alive. Without it, a data pointer load
|
||||
* ahead of a bounds branch is sunk into the in-bounds block, and LICM keeps
|
||||
* a conditionally executed load inside the loop. */
|
||||
export function bytesHeaderAlias(host: LlvmEmitterContext): string {
|
||||
const tag = host.memoryAliasAttachment("bytes:header");
|
||||
return tag === "" ? "" : `${tag}, !invariant.load !{}`;
|
||||
}
|
||||
|
||||
/** A header load whose value the program reads (`length`, `byteLength`)
|
||||
* keeps the alias tag only. The value can outlive the view, and LLVM may
|
||||
* move an invariant load to its use, past the release that frees the
|
||||
* view. */
|
||||
export function bytesHeaderValueAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:header");
|
||||
}
|
||||
|
||||
/** Element storage's tag, and the alias scope of the access being emitted
|
||||
* in a loop version's fast copy (bytesAccessScope). */
|
||||
export function bytesElementAlias(host: LlvmEmitterContext): string {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/* Typed-array header fields read once at a speculative loop's entry.
|
||||
*
|
||||
* A view's length, data pointer and shared storage are set at construction
|
||||
* and never change, and a view held by a const local or a parameter that
|
||||
* the loop does not rebind is the same view in every iteration. The entry
|
||||
* reads those fields once, and every element access in the loop's copies
|
||||
* (the ordinary copy, the fast copy and the copy a failed check resumes in)
|
||||
* uses the values. No header read is then left inside the copies for LLVM's
|
||||
* load PRE to move ahead of the fast copy's exits: such a move keeps the
|
||||
* fast loop's first iteration in the loop, with the checks that one
|
||||
* iteration of it makes redundant. */
|
||||
import type { IrExpr, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmt } from "../../ir/traverse.js";
|
||||
import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
|
||||
export interface BytesHeader {
|
||||
length: string;
|
||||
data: string;
|
||||
/** The view's shared storage (worker programs only). */
|
||||
shared: string | null;
|
||||
}
|
||||
|
||||
export interface BytesHeaderScope {
|
||||
/** The function the scope's values belong to. */
|
||||
fn: string;
|
||||
locals: ReadonlyMap<string, BytesHeader>;
|
||||
/** The header of each receiver value read from one of `locals`. */
|
||||
values: Map<string, BytesHeader>;
|
||||
}
|
||||
|
||||
/** Reads the header fields of the views `loop` accesses through a const
|
||||
* local or a parameter it does not rebind, at the current block, which must
|
||||
* dominate every copy of the loop. Null when there are none. */
|
||||
export function emitLoopBytesHeaders(
|
||||
emitter: LlEmitter,
|
||||
loop: IrStmt,
|
||||
parameters: ReadonlySet<string>,
|
||||
): BytesHeaderScope | null {
|
||||
const receivers = new Set<string>();
|
||||
const rebound = new Set<string>();
|
||||
const receiver = (e: IrExpr): void => {
|
||||
if (e.kind === "varRef") receivers.add(e.localId);
|
||||
};
|
||||
everyStmt(loop, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "bytesIntrinsic") receiver(e.receiver);
|
||||
else if (e.kind === "assignExpr" || e.kind === "incDec") rebound.add(e.localId);
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "bytesSet") receiver(s.arr);
|
||||
else if (s.kind === "assign" || s.kind === "forOf" || s.kind === "varDecl")
|
||||
rebound.add(s.localId);
|
||||
else if (s.kind === "tryCatch" && s.catchLocalId !== null) rebound.add(s.catchLocalId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const locals = new Map<string, BytesHeader>();
|
||||
for (const id of receivers) {
|
||||
if (rebound.has(id)) continue;
|
||||
const local = emitter.currentLocals.get(id);
|
||||
if (local === undefined || local.boxed === true || local.type.kind !== "bytes") continue;
|
||||
if (local.mutable && !parameters.has(id)) continue;
|
||||
if (emitter.integerLoopBindings.has(id) || emitter.int32Locals.has(id)) continue;
|
||||
const B = emitter.B;
|
||||
const view = B.tmp();
|
||||
B.line(`${view} = load ptr, ptr ${emitter.binding(id).slot}${emitter.slotAlias(id)}`);
|
||||
locals.set(id, {
|
||||
length: emitBytesHeaderLength(emitter, view),
|
||||
data: emitBytesData(emitter, view),
|
||||
shared: emitter.mod.workers ? emitBytesSharedStorage(emitter, view) : null,
|
||||
});
|
||||
}
|
||||
if (locals.size === 0) return null;
|
||||
return { fn: emitter.currentFunctionName, locals, values: new Map() };
|
||||
}
|
||||
@@ -317,13 +317,20 @@ test("worker programs test each receiver's shared storage once in the guard", ()
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
|
||||
expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2);
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.fast\.shared\d+:$/m.exec(body)![0];
|
||||
expect(fast).not.toContain("bytes.shared");
|
||||
expect(fast).not.toMatch(/(load|store) atomic double/);
|
||||
// The checked copy keeps the shared-memory path for each access.
|
||||
// The checked copy keeps the shared-memory path for each access. The copy
|
||||
// for shared views whose bounds hold is a counted loop that polls between
|
||||
// blocks, so its accesses at the counter are plain.
|
||||
const slow = body.slice(body.search(/^bytes\.version\.slow\d+:$/m), body.indexOf(fast));
|
||||
expect(slow).toContain("load atomic double");
|
||||
expect(slow).toContain("store atomic double");
|
||||
const shared = /^bytes\.version\.fast\.shared\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(
|
||||
body,
|
||||
)![0];
|
||||
expect(shared).toMatch(/^loop\.poll\d+:$/m);
|
||||
expect(shared).not.toMatch(/(load|store) atomic double/);
|
||||
});
|
||||
|
||||
test("index temps of a version's counter are integers in the fast copy", () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import {
|
||||
emitStringAsciiFlag,
|
||||
emitStringByteLength,
|
||||
@@ -85,8 +86,15 @@ export interface BytesLoopVersion {
|
||||
/** Root receiver bindings, read once before the loop; `string` marks a
|
||||
* string whose `charCodeAt` and `charAt` reads the version covers. */
|
||||
receivers: { ref: VarRef; global: boolean; string: boolean }[];
|
||||
/** Receivers the guard requires to have no shared storage. */
|
||||
/** Receivers the guard requires to have no shared storage for the fast
|
||||
* copy. */
|
||||
ownedReceivers: number[];
|
||||
/** Worker programs: when the bounds hold but a receiver in
|
||||
* `ownedReceivers` has shared storage, the loop runs a second fast copy
|
||||
* instead of the checked one. It skips the bounds checks of `bounds` and
|
||||
* keeps shared-safe accesses (emitBytesLoopVersionGuard's `sharedFast`). Set
|
||||
* when the version proves bounds as well as ownership. */
|
||||
sharedCopy: boolean;
|
||||
/** The string reads among `bounds`. */
|
||||
stringReads: ReadonlySet<Node>;
|
||||
/** The version's one string receiver, when the loop may also run over
|
||||
@@ -752,6 +760,7 @@ export function matchBytesLoopVersion(
|
||||
owned: ownedNodes,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
|
||||
sharedCopy: ownedKept.length > 0 && bounds.size > 0,
|
||||
stringReads,
|
||||
unitsReceiver,
|
||||
unitLengths: unitLengths ?? new Set<IrExpr>(),
|
||||
@@ -1102,6 +1111,10 @@ function findWhileCounterAccesses(
|
||||
* storage, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set).
|
||||
*
|
||||
* With `sharedFast` (a version with `sharedCopy`), the ownership tests are
|
||||
* separate: when everything else holds but a view has shared storage, the
|
||||
* guard branches to `sharedFast` instead of `slow`.
|
||||
*
|
||||
* With `unitsFast`, the version's units receiver may instead be a string
|
||||
* that is not ASCII: the guard has the runtime publish it as the scan
|
||||
* receiver (one read), tests the same accesses against its UTF-16 length,
|
||||
@@ -1115,6 +1128,7 @@ export function emitBytesLoopVersionGuard(
|
||||
fast: string,
|
||||
slow: string,
|
||||
unitsFast?: string,
|
||||
sharedFast?: string,
|
||||
): { units: string | null; length: string | null } | null {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
@@ -1146,7 +1160,7 @@ export function emitBytesLoopVersionGuard(
|
||||
};
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
if (index === unitsIndex) {
|
||||
const read = emitUnitsLength(host, receiver);
|
||||
const read = emitUnitsLength(host, receiver, () => emitUnitsSpan(host, version, index));
|
||||
units.read = read;
|
||||
return read.length;
|
||||
}
|
||||
@@ -1165,10 +1179,13 @@ export function emitBytesLoopVersionGuard(
|
||||
if (string && index !== unitsIndex)
|
||||
conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
});
|
||||
// Without `sharedFast`, shared storage sends the loop to the checked copy
|
||||
// with the other failed tests; with it, only to the shared fast copy.
|
||||
const ownership: string[] = [];
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`);
|
||||
conditions.push(owned);
|
||||
(sharedFast !== undefined ? ownership : conditions).push(owned);
|
||||
}
|
||||
for (const check of version.fixed) {
|
||||
const ok = B.tmp();
|
||||
@@ -1351,7 +1368,22 @@ export function emitBytesLoopVersionGuard(
|
||||
const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null;
|
||||
const taken = units.read;
|
||||
const target = taken ? B.newLabel("bytes.version.units") : fast;
|
||||
if (valid === "true") B.br(target);
|
||||
if (sharedFast !== undefined && taken)
|
||||
throw new InternalCompilerError("llvm emitter bug: a shared fast copy with a units copy");
|
||||
if (sharedFast !== undefined && ownership.length > 0) {
|
||||
// The bounds hold: unshared views take the owned copy.
|
||||
const split = B.newLabel("bytes.version.owned");
|
||||
let owned = ownership[0]!;
|
||||
for (const cond of ownership.slice(1)) {
|
||||
const all = B.tmp();
|
||||
B.line(`${all} = and i1 ${owned}, ${cond}`);
|
||||
owned = all;
|
||||
}
|
||||
if (valid === "true") B.br(split);
|
||||
else B.condBr(valid, split, slow);
|
||||
B.startBlock(split);
|
||||
B.condBr(owned, target, sharedFast);
|
||||
} else if (valid === "true") B.br(target);
|
||||
else B.condBr(valid, target, slow);
|
||||
if (!taken) return { units: null, length };
|
||||
B.startBlock(target);
|
||||
@@ -1359,15 +1391,49 @@ export function emitBytesLoopVersionGuard(
|
||||
return { units: taken.pointer, length };
|
||||
}
|
||||
|
||||
/** The range of indexes a version's loop reads from its units receiver, as
|
||||
* two doubles: a counted loop's start and limit, or a while loop's one
|
||||
* counter and the receiver's bound. Both NaN when the version gives no such
|
||||
* range. The runtime converts the receiver at once only when the range is
|
||||
* long enough to pay for it (scr_str_scan_units_for). */
|
||||
function emitUnitsSpan(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
receiver: number,
|
||||
): { from: string; to: string } {
|
||||
// Locals and literals only: they read no string (a `length` of the
|
||||
// receiver would measure it). Anything else leaves the range unbounded.
|
||||
const number = (expr: IrExpr): string | null => {
|
||||
if (expr.kind !== "varRef" && expr.kind !== "numLit") return null;
|
||||
const value = host.emitExpr(expr);
|
||||
return value.type.kind === "f64" ? value.name : null;
|
||||
};
|
||||
const unbounded = { from: "0x7FF8000000000000", to: "0x7FF8000000000000" };
|
||||
const counted = version.counted;
|
||||
let from: IrExpr, to: IrExpr;
|
||||
if (counted) [from, to] = [counted.start, counted.limit];
|
||||
else {
|
||||
const bounded = version.bounded.filter((check) => check.receiver === receiver);
|
||||
if (version.counters.length !== 1 || bounded.length !== 1 || version.paired !== null)
|
||||
return unbounded;
|
||||
[from, to] = [version.counters[0]!.ref, bounded[0]!.bound];
|
||||
}
|
||||
const start = number(from),
|
||||
limit = number(to);
|
||||
return start !== null && limit !== null ? { from: start, to: limit } : unbounded;
|
||||
}
|
||||
|
||||
/** The length of a version's units receiver `s` in UTF-16 units: its byte
|
||||
* length when it is ASCII; otherwise, once it is the scan receiver, the
|
||||
* published unit count (0 when the runtime does not hold its units). When
|
||||
* `s` is not already the scan receiver, the runtime converts it to units
|
||||
* and publishes it (scr_str_scan_units). Also the units pointer, whether
|
||||
* the loop is to read it, and whether the published receiver is `s`. */
|
||||
* and publishes it if the loop's `span` makes that pay
|
||||
* (scr_str_scan_units_for). Also the units pointer, whether the loop is to
|
||||
* read it, and whether the published receiver is `s`. */
|
||||
function emitUnitsLength(
|
||||
host: LlvmEmitterContext,
|
||||
s: string,
|
||||
span: () => { from: string; to: string },
|
||||
): { length: string; pointer: string; taken: string; published: string } {
|
||||
const B = host.B;
|
||||
const sz = host.sizeType;
|
||||
@@ -1381,14 +1447,15 @@ function emitUnitsLength(
|
||||
B.startBlock(publish);
|
||||
// When `s` is already the scan receiver (an earlier loop over it published
|
||||
// it), its units stay published until the runtime clears the scan: skip
|
||||
// the call.
|
||||
// the call. Loops re-entered over one string take that path; the weights
|
||||
// keep the call and its reach out of the loop's code.
|
||||
const held = B.tmp(),
|
||||
holds = B.tmp();
|
||||
B.line(`${held} = load ptr, ptr @scr_str_scan`);
|
||||
B.line(`${holds} = icmp eq ptr ${held}, ${s}`);
|
||||
const call = B.newLabel("bytes.version.publish.units"),
|
||||
load = B.newLabel("bytes.version.publish.load");
|
||||
B.condBr(holds, load, call);
|
||||
B.condBr(holds, load, call, `, !prof !{!"branch_weights", i32 1000, i32 1}`);
|
||||
B.startBlock(call);
|
||||
host.declare(STRING_SCAN_UNITS_DECL);
|
||||
const receiver = B.tmp(),
|
||||
@@ -1398,7 +1465,8 @@ function emitUnitsLength(
|
||||
countSlot = B.tmp(),
|
||||
count = B.tmp(),
|
||||
published = B.tmp();
|
||||
B.line(`call void @scr_str_scan_units(ptr ${s})`);
|
||||
const { from: first, to: bound } = span();
|
||||
B.line(`call void @scr_str_scan_units_for(ptr ${s}, double ${first}, double ${bound})`);
|
||||
B.br(load);
|
||||
B.startBlock(load);
|
||||
B.line(`${receiver} = load ptr, ptr @scr_str_scan`);
|
||||
|
||||
@@ -101,6 +101,13 @@ export class CheckedNarrows {
|
||||
}
|
||||
}
|
||||
|
||||
/** A call to a checked extraction whose helper borrows its argument, so
|
||||
* emitCheckedNarrowReceiver can lend the payload instead of calling it. */
|
||||
export function borrowsCheckedNarrow(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "call" || host.checkedNarrows.get(e) === null) return false;
|
||||
return host.callLifetimes.borrowed.get(e.callee)?.has(0) === true && host.mayThrow.has(e.callee);
|
||||
}
|
||||
|
||||
/** A checked extraction consumed at once by a field or tag load. The
|
||||
* successful arm borrows the payload from the union, which itself borrows
|
||||
* from its source: a dense array read keeps the element in the array slot,
|
||||
@@ -113,7 +120,7 @@ export function emitCheckedNarrowReceiver(
|
||||
narrow: CheckedNarrowCall,
|
||||
): LlValue | null {
|
||||
const callee = narrow.call.callee;
|
||||
if (!host.callLifetimes.borrowed.get(callee)?.has(0) || !host.mayThrow.has(callee)) return null;
|
||||
if (!borrowsCheckedNarrow(host, narrow.call)) return null;
|
||||
const B = host.B;
|
||||
const read = host.optionalArrayReads.get(narrow.value);
|
||||
const union = read
|
||||
|
||||
@@ -573,13 +573,21 @@ export function classIdLoadLines(
|
||||
* 64-bit word. The count is the i32 at offset 0. */
|
||||
export const CLASS_ID_OFFSET = 4;
|
||||
|
||||
/** The bound on layout ids (scr_rc.h SCR_RC_LAYOUT_ID_LIMIT): the high
|
||||
* half of a 64-bit rc word keeps the layout id in its low 30 bits, beside
|
||||
* the program-lifetime and frozen bits. A class id is the layout id of its
|
||||
* class's canonical layout. */
|
||||
export const LAYOUT_ID_LIMIT = 0x3fffffff;
|
||||
|
||||
/** The rc word an LlClassMeta.idInRc instance is created with: the class
|
||||
* id, its preorder number plus 1, in the high 32 bits, and in the low 32 bits
|
||||
* a count of 1, or for a program-lifetime instance the midpoint of the 32-bit
|
||||
* count (as programLifetimeRc is of the 32-bit word). The id is never 0,
|
||||
* 0x80000000 or 0xffffffff, the high halves of other objects' rc words, so
|
||||
* the runtime can tell an object with a class id (scr_rc.h scr_rc_class_id). */
|
||||
* count (as programLifetimeRc is of the 32-bit word). The id is a layout id
|
||||
* (LAYOUT_ID_LIMIT), so it never sets the program-lifetime or frozen bit
|
||||
* and the runtime reads it back (scr_rc.h scr_rc_layout_id). */
|
||||
export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string {
|
||||
if (meta.pre + 1 >= LAYOUT_ID_LIMIT)
|
||||
throw new InternalCompilerError(`class id ${meta.pre + 1} is not a layout id`);
|
||||
return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,10 @@ export function emitFieldAbsentTest(B: BlockBuilder, value: string, undefinedTag
|
||||
return absent;
|
||||
}
|
||||
|
||||
/** ScrClosure.function_kind's SCR_FN_RECEIVER_FREE bit (scr_closure.h):
|
||||
* the body never observes the ambient receiver. */
|
||||
export const CLOSURE_RECEIVER_FREE = 16;
|
||||
|
||||
/** The right-hand side of a JS function-identity comparison: two closures
|
||||
* are one function when their identity roots match (a signature adapter
|
||||
* stands for the function it adapts). Both operands must be closures. */
|
||||
|
||||
@@ -299,6 +299,47 @@ test("receiver readers propagate backwards through direct calls and recursion",
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("signature adapters that only forward the receiver ignore it with their original", () => {
|
||||
const forward = (receiver: IrExpr): IrStmt => ({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "callValue",
|
||||
callee: ref("f.0"),
|
||||
receiver,
|
||||
args: [{ kind: "varRef", localId: "a.0", type: F64, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
const ambient: IrExpr = { kind: "libCall", fn: "dyn.this", args: [], type: { kind: "dyn" }, loc };
|
||||
const adapter: IrFunction = {
|
||||
name: "%fn.adapt.x.impl",
|
||||
params: [{ localId: "a.0", name: "a", type: F64 }],
|
||||
captures: [{ localId: "f.0", name: "f", type: callback }],
|
||||
locals: [
|
||||
{ id: "f.0", name: "f", type: callback, mutable: false, boxed: true },
|
||||
{ id: "a.0", name: "a", type: F64, mutable: false },
|
||||
],
|
||||
returnType: BOOL,
|
||||
body: [forward(ambient)],
|
||||
loc,
|
||||
};
|
||||
const readers = (): AmbientReceiverReaders =>
|
||||
new AmbientReceiverReaders(new Map([[adapter.name, adapter]]));
|
||||
// The adapter itself reads the receiver, only to forward it.
|
||||
expect(readers().ignores(adapter)).toBe(false);
|
||||
expect(readers().forwardsOnly(adapter)).toBe(true);
|
||||
// Any other read of the receiver, or forwarding something else, does not.
|
||||
adapter.body = [thisRead, forward(ambient)];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
adapter.body = [forward(ref("a.0", F64))];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
adapter.body = [forward(ambient)];
|
||||
adapter.captures = [];
|
||||
expect(readers().forwardsOnly(adapter)).toBe(false);
|
||||
});
|
||||
|
||||
test("virtual calls admit every same-named method", () => {
|
||||
const call: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import { matchConstantChoice } from "./constant-choices.js";
|
||||
|
||||
const loc = { file: "choices.ts", start: 0, end: 0 };
|
||||
const bytes = { kind: "bytes", elem: "u8" } as const;
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string, type: IrExpr["type"] = F64): IrExpr => ({
|
||||
kind: "varRef",
|
||||
localId,
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
const is = (left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left,
|
||||
right,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const store = (value: IrExpr, index: IrExpr = ref("i"), arr = "a"): IrStmt => ({
|
||||
kind: "bytesSet",
|
||||
arr: ref(arr, bytes),
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const branch = (cond: IrExpr, then: IrStmt[], else_: IrStmt[] | null): IrStmt => ({
|
||||
kind: "if",
|
||||
cond,
|
||||
then,
|
||||
else_,
|
||||
loc,
|
||||
});
|
||||
/** `if (x === c0) a[i] = r0; else if ... else a[i] = fallback`. */
|
||||
function chain(cases: [number, number][], fallback: IrStmt[] | null, subject = "x"): IrStmt {
|
||||
let rest = fallback;
|
||||
for (let k = cases.length - 1; k >= 0; k--) {
|
||||
const [value, result] = cases[k]!;
|
||||
rest = [branch(is(ref(subject), num(value)), [store(num(result))], rest)];
|
||||
}
|
||||
return rest![0]!;
|
||||
}
|
||||
|
||||
test("a chain of literal stores to one element is a constant choice", () => {
|
||||
const choice = matchConstantChoice(
|
||||
chain(
|
||||
[
|
||||
[97, 0],
|
||||
[116, 1],
|
||||
[99, 2],
|
||||
],
|
||||
[store(num(3))],
|
||||
),
|
||||
);
|
||||
expect(choice?.cases).toEqual([
|
||||
{ value: 97, result: 0 },
|
||||
{ value: 116, result: 1 },
|
||||
{ value: 99, result: 2 },
|
||||
]);
|
||||
expect(choice?.fallback).toBe(3);
|
||||
expect(choice?.stores).toHaveLength(4);
|
||||
});
|
||||
|
||||
test("a literal on the left, -0 and a repeated value keep strict equality's first arm", () => {
|
||||
const first: IrStmt = branch(
|
||||
is(num(-0), ref("x")),
|
||||
[store(num(5))],
|
||||
[
|
||||
chain(
|
||||
[
|
||||
[1, 6],
|
||||
[0, 7],
|
||||
[2, 8],
|
||||
],
|
||||
[store(num(9))],
|
||||
),
|
||||
],
|
||||
);
|
||||
expect(matchConstantChoice(first)?.cases).toEqual([
|
||||
{ value: 0, result: 5 },
|
||||
{ value: 1, result: 6 },
|
||||
{ value: 2, result: 8 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("chains that are not one choice of literals keep their arms", () => {
|
||||
const three: [number, number][] = [
|
||||
[1, 10],
|
||||
[2, 20],
|
||||
[3, 30],
|
||||
];
|
||||
// Two values, no final else, a fraction, or cases spanning more than 256 values.
|
||||
expect(matchConstantChoice(chain(three.slice(0, 2), [store(num(0))]))).toBeNull();
|
||||
expect(matchConstantChoice(chain(three, null))).toBeNull();
|
||||
expect(matchConstantChoice(chain([...three, [1.5, 40]], [store(num(0))]))).toBeNull();
|
||||
expect(matchConstantChoice(chain([...three, [256, 40]], [store(num(0))]))).not.toBeNull();
|
||||
expect(matchConstantChoice(chain([...three, [257, 40]], [store(num(0))]))).toBeNull();
|
||||
// A final else that is not a literal, or that stores elsewhere.
|
||||
expect(matchConstantChoice(chain(three, [store(ref("y"))]))).toBeNull();
|
||||
expect(matchConstantChoice(chain(three, [store(num(0), ref("j"))]))).toBeNull();
|
||||
expect(matchConstantChoice(chain(three, [store(num(0), ref("i"), "b")]))).toBeNull();
|
||||
// An arm with a second statement.
|
||||
expect(matchConstantChoice(chain(three, [store(num(0)), store(num(1))]))).toBeNull();
|
||||
// A second subject.
|
||||
const mixed = branch(is(ref("y"), num(0)), [store(num(1))], [chain(three, [store(num(0))])]);
|
||||
expect(matchConstantChoice(mixed)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import type { IrExpr, IrStmt } from "../../ir/ir.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
|
||||
type BytesSet = Extract<IrStmt, { kind: "bytesSet" }>;
|
||||
|
||||
/** An if/else-if chain that stores one of several numeric literals to the
|
||||
* same typed-array element, choosing it by comparing one number local with
|
||||
* integer literals:
|
||||
*
|
||||
* if (x === 97) a[i] = 0; else if (x === 116) a[i] = 1; ... else a[i] = 3;
|
||||
*
|
||||
* The chain is emitted as one store of a value read from a constant table,
|
||||
* with no branch on the comparisons. Branches on unpredictable input (the
|
||||
* bases of a DNA sequence, the bytes of a text) mispredict about half the
|
||||
* time; LLVM keeps them, because sparse cases (97, 99, 116) are too far
|
||||
* apart for its own switch tables. */
|
||||
export interface ConstantChoice {
|
||||
subject: IrExpr;
|
||||
/** The first arm for each distinct value, in chain order. */
|
||||
cases: readonly { value: number; result: number }[];
|
||||
fallback: number;
|
||||
/** Every arm's store, the final else's last. */
|
||||
stores: readonly BytesSet[];
|
||||
}
|
||||
|
||||
/** At least this many distinct values: shorter chains predict as well as a
|
||||
* table load. */
|
||||
const MIN_CASES = 3;
|
||||
/** The table covers the subject's values from the smallest case (or the
|
||||
* subject's proven minimum) up to this many entries. */
|
||||
const MAX_TABLE = 256;
|
||||
|
||||
function integerLiteral(expr: IrExpr): number | null {
|
||||
if (expr.kind !== "numLit" || !Number.isSafeInteger(expr.value)) return null;
|
||||
// `x === -0` and `x === 0` select the same values.
|
||||
return expr.value === 0 ? 0 : expr.value;
|
||||
}
|
||||
|
||||
function sameVarRef(a: IrExpr, b: IrExpr): boolean {
|
||||
return a.kind === "varRef" && b.kind === "varRef" && a.localId === b.localId;
|
||||
}
|
||||
|
||||
/** A local read or a number literal: reading it has no effect. */
|
||||
function sameOperand(a: IrExpr, b: IrExpr): boolean {
|
||||
if (a.kind === "numLit" && b.kind === "numLit") return Object.is(a.value, b.value);
|
||||
return sameVarRef(a, b);
|
||||
}
|
||||
|
||||
/** The store of a single-statement arm, when it stores a number literal to
|
||||
* an element named by a local and a local or a literal index (so evaluating
|
||||
* them once, after the comparisons, reads what every arm would). */
|
||||
function literalStore(body: readonly IrStmt[] | null): BytesSet | null {
|
||||
if (body === null || body.length !== 1) return null;
|
||||
const s = body[0]!;
|
||||
if (s.kind !== "bytesSet" || s.value.kind !== "numLit") return null;
|
||||
if (s.arr.kind !== "varRef" || (s.index.kind !== "varRef" && s.index.kind !== "numLit"))
|
||||
return null;
|
||||
if (s.arr.type.kind !== "bytes" || s.index.type.kind !== "f64") return null;
|
||||
return s;
|
||||
}
|
||||
|
||||
export function matchConstantChoice(s: IrStmt): ConstantChoice | null {
|
||||
if (s.kind !== "if") return null;
|
||||
let subject: IrExpr | null = null;
|
||||
const cases: { value: number; result: number }[] = [];
|
||||
const seen = new Set<number>();
|
||||
const stores: BytesSet[] = [];
|
||||
let current: IrStmt = s;
|
||||
for (;;) {
|
||||
if (current.kind !== "if") return null;
|
||||
const cond = current.cond;
|
||||
if (cond.kind !== "bin" || cond.op !== "===") return null;
|
||||
const [local, literal] =
|
||||
cond.left.kind === "varRef" ? [cond.left, cond.right] : [cond.right, cond.left];
|
||||
if (local.kind !== "varRef" || local.type.kind !== "f64") return null;
|
||||
if (subject === null) subject = local;
|
||||
else if (!sameVarRef(subject, local)) return null;
|
||||
const value = integerLiteral(literal);
|
||||
if (value === null) return null;
|
||||
const store = literalStore(current.then);
|
||||
if (store === null) return null;
|
||||
stores.push(store);
|
||||
// A repeated value never reaches its later arm.
|
||||
if (!seen.has(value)) {
|
||||
seen.add(value);
|
||||
cases.push({ value, result: (store.value as IrExpr & { kind: "numLit" }).value });
|
||||
}
|
||||
const rest: readonly IrStmt[] | null = current.else_;
|
||||
if (rest === null || rest.length !== 1) return null;
|
||||
if (rest[0]!.kind === "if") {
|
||||
current = rest[0]!;
|
||||
continue;
|
||||
}
|
||||
const last = literalStore(rest);
|
||||
if (last === null) return null;
|
||||
stores.push(last);
|
||||
break;
|
||||
}
|
||||
const first = stores[0]!;
|
||||
for (const store of stores) {
|
||||
if (!sameVarRef(store.arr, first.arr) || !sameOperand(store.index, first.index)) return null;
|
||||
if (store.arr.type.kind !== "bytes" || first.arr.type.kind !== "bytes") return null;
|
||||
if (store.arr.type.elem !== first.arr.type.elem) return null;
|
||||
}
|
||||
if (cases.length < MIN_CASES) return null;
|
||||
const values = cases.map((c) => c.value);
|
||||
if (Math.max(...values) - Math.min(...values) >= MAX_TABLE) return null;
|
||||
const fallback = (stores[stores.length - 1]!.value as IrExpr & { kind: "numLit" }).value;
|
||||
return { subject: subject!, cases, fallback, stores };
|
||||
}
|
||||
|
||||
function int32(value: number): boolean {
|
||||
return (
|
||||
Number.isInteger(value) && value >= -2147483648 && value <= 2147483647 && !Object.is(value, -0)
|
||||
);
|
||||
}
|
||||
|
||||
/** The chosen number: the table entry for the subject's value, or the final
|
||||
* else's literal for a value no case names (including NaN, a fraction and
|
||||
* every value outside the table). */
|
||||
function emitChoice(host: LlvmEmitterContext, choice: ConstantChoice): LlValue {
|
||||
const B = host.B;
|
||||
const subject = host.emitExpr(choice.subject);
|
||||
const results = [...choice.cases.map((c) => c.result), choice.fallback];
|
||||
const integral = results.every(int32);
|
||||
const element = integral ? "i32" : "double";
|
||||
const literal = (value: number): string => (integral ? String(value) : f64Lit(value));
|
||||
|
||||
const proven = exactInteger(host, subject, choice.subject);
|
||||
const values = choice.cases.map((c) => c.value);
|
||||
let low = Math.min(...values);
|
||||
let high = Math.max(...values);
|
||||
// A proven range that fits a table needs no test: every value it allows
|
||||
// has an entry.
|
||||
const covered =
|
||||
proven !== null && proven.range.max - proven.range.min < MAX_TABLE ? proven : null;
|
||||
if (covered !== null) {
|
||||
low = covered.range.min;
|
||||
high = covered.range.max;
|
||||
}
|
||||
const size = high - low + 1;
|
||||
const entries: string[] = [];
|
||||
for (let value = low; value <= high; value++) {
|
||||
const hit = choice.cases.find((c) => c.value === value);
|
||||
entries.push(`${element} ${literal(hit === undefined ? choice.fallback : hit.result)}`);
|
||||
}
|
||||
const table = `@sc_choice_${createHash("sha256")
|
||||
.update(`${element}:${entries.join(",")}`)
|
||||
.digest("hex")
|
||||
.slice(0, 16)}`;
|
||||
host.declare(
|
||||
`${table} = private unnamed_addr constant [${size} x ${element}] [${entries.join(", ")}]`,
|
||||
);
|
||||
|
||||
let index: string;
|
||||
let ok: string | null = null;
|
||||
if (covered !== null) {
|
||||
const wide = widenInteger(host, covered);
|
||||
index = B.tmp();
|
||||
B.line(`${index} = sub i64 ${wide}, ${low}`);
|
||||
} else {
|
||||
// freeze keeps NaN and out-of-range conversions from producing poison;
|
||||
// the round trip then rejects every value that is not an exact integer
|
||||
// (-0 converts to 0, which `=== 0` selects too).
|
||||
const raw = B.tmp();
|
||||
const selector = B.tmp();
|
||||
const restored = B.tmp();
|
||||
const exact = B.tmp();
|
||||
const offset = B.tmp();
|
||||
const inside = B.tmp();
|
||||
ok = B.tmp();
|
||||
index = B.tmp();
|
||||
B.line(`${raw} = fptosi double ${subject.name} to i64`);
|
||||
B.line(`${selector} = freeze i64 ${raw}`);
|
||||
B.line(`${restored} = sitofp i64 ${selector} to double`);
|
||||
B.line(`${exact} = fcmp oeq double ${subject.name}, ${restored}`);
|
||||
B.line(`${offset} = sub i64 ${selector}, ${low}`);
|
||||
B.line(`${inside} = icmp ult i64 ${offset}, ${size}`);
|
||||
B.line(`${ok} = and i1 ${exact}, ${inside}`);
|
||||
B.line(`${index} = select i1 ${ok}, i64 ${offset}, i64 0`);
|
||||
}
|
||||
const slot = B.tmp();
|
||||
const entry = B.tmp();
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds [${size} x ${element}], ptr ${table}, i64 0, i64 ${index}`,
|
||||
);
|
||||
B.line(`${entry} = load ${element}, ptr ${slot}`);
|
||||
let chosen = entry;
|
||||
if (ok !== null) {
|
||||
chosen = B.tmp();
|
||||
B.line(
|
||||
`${chosen} = select i1 ${ok}, ${element} ${entry}, ${element} ${literal(choice.fallback)}`,
|
||||
);
|
||||
}
|
||||
if (!integral) return { name: chosen, type: choice.stores[0]!.value.type };
|
||||
const number = B.tmp();
|
||||
B.line(`${number} = sitofp i32 ${chosen} to double`);
|
||||
return {
|
||||
name: number,
|
||||
type: choice.stores[0]!.value.type,
|
||||
uint32: chosen,
|
||||
integer: {
|
||||
name: chosen,
|
||||
type: "i32",
|
||||
signed: Math.min(...results) < 0,
|
||||
range: { min: Math.min(...results), max: Math.max(...results) },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Emits a matched chain as one store of the chosen literal. The element
|
||||
* write is the arms' own: every arm names the same receiver and index. */
|
||||
export function emitConstantChoice(host: LlvmEmitterContext, choice: ConstantChoice): void {
|
||||
const first = choice.stores[0]!;
|
||||
if (first.arr.type.kind !== "bytes")
|
||||
throw new InternalCompilerError("llvm emitter bug: constant choice on non-bytes");
|
||||
// An arm's index carries the ranges proven inside that arm: an index that
|
||||
// is the compared local holds the arm's value there. The one store runs
|
||||
// for every value, so such an index reads the local through the first
|
||||
// comparison's operand, whose range holds before the chain branches.
|
||||
const indexExpr = sameVarRef(first.index, choice.subject) ? choice.subject : first.index;
|
||||
const arr = host.emitStableReceiver(first.arr, [indexExpr]);
|
||||
const index = host.emitExpr(indexExpr);
|
||||
const value = emitChoice(host, choice);
|
||||
const scopes = choice.stores.map((store) => host.bytesAliasScopes.get(store));
|
||||
host.emitBytesSet(
|
||||
first.arr.type.elem,
|
||||
arr.name,
|
||||
index,
|
||||
value,
|
||||
indexExpr,
|
||||
undefined,
|
||||
choice.stores.every((store) => host.bytesBounds.has(store)),
|
||||
choice.stores.every((store) => host.bytesOwned.has(store)),
|
||||
scopes.every((scope) => scope === scopes[0]) ? (scopes[0] ?? "") : "",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/* Runtime CPU dispatch for x86-64 executables (cloneForCpuDispatch): the
|
||||
* program's functions get a copy compiled for x86-64-v3, chosen once per
|
||||
* call by the CPU level the executable detects at startup. */
|
||||
import type { TargetCpu } from "../target-cpu.js";
|
||||
|
||||
/** The levels a dispatching executable carries a copy of its program's
|
||||
* functions for, above the baseline. */
|
||||
export const CPU_DISPATCH_TIERS: readonly TargetCpu[] = ["x86-64-v3"];
|
||||
|
||||
/** The CPU level the executable runs at: 1 (the x86-64 baseline) or 3
|
||||
* (x86-64-v3), set by @sc_cpu_level_init before the program runs. */
|
||||
const LEVEL = "@sc_cpu_level";
|
||||
|
||||
/** The attribute groups of the x86-64-v3 copies: #0's and #2's (see
|
||||
* emit-module.ts functionAttributes) with the level's `target-cpu`. */
|
||||
const COPY_GROUPS: ReadonlyMap<string, string> = new Map([
|
||||
["0", "3"],
|
||||
["2", "4"],
|
||||
]);
|
||||
|
||||
export function cpuDispatchAttributes(asan: string): string[] {
|
||||
return [
|
||||
`attributes #3 = {${asan} "target-cpu"="x86-64-v3" }`,
|
||||
`attributes #4 = { noinline${asan} "target-cpu"="x86-64-v3" }`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Bits of CPUID leaf 1's ECX that x86-64-v3 needs: SSE3 (0), SSSE3 (9),
|
||||
* FMA (12), CMPXCHG16B (13), SSE4.1 (19), SSE4.2 (20), MOVBE (22), POPCNT
|
||||
* (23), XSAVE (26), OSXSAVE (27), AVX (28) and F16C (29). */
|
||||
const LEAF1_ECX = 0x3cd83201;
|
||||
/** Leaf 7's EBX: BMI1 (3), AVX2 (5) and BMI2 (8). */
|
||||
const LEAF7_EBX = 0x128;
|
||||
/** Leaf 0x80000001's ECX: LAHF/SAHF (0) and LZCNT (5). */
|
||||
const EXT1_ECX = 0x21;
|
||||
|
||||
/** `SCRIPTC_CPU_LEVEL` values and the level each caps the executable at. */
|
||||
const LEVEL_NAMES: readonly [string, number][] = [
|
||||
["0", 0],
|
||||
["1", 1],
|
||||
["2", 2],
|
||||
["3", 3],
|
||||
["4", 4],
|
||||
["baseline", 1],
|
||||
["v1", 1],
|
||||
["v2", 2],
|
||||
["v3", 3],
|
||||
["v4", 4],
|
||||
["x86-64", 1],
|
||||
["x86-64-v2", 2],
|
||||
["x86-64-v3", 3],
|
||||
["x86-64-v4", 4],
|
||||
];
|
||||
|
||||
/** @sc_cpu_level and @sc_cpu_level_init, which main calls first:
|
||||
* - CPUID reports every x86-64-v3 feature, and XGETBV that the operating
|
||||
* system saves the SSE and AVX registers, for level 3; otherwise 1;
|
||||
* - `SCRIPTC_CPU_LEVEL` (a level from 0 to 4, `baseline` or an `x86-64-vN`
|
||||
* name) caps the level; another value caps it at 0. */
|
||||
export function cpuLevelDefs(declare: (line: string) => void): string[] {
|
||||
declare(`declare ptr @getenv(ptr)`);
|
||||
declare(`declare i32 @strcmp(ptr, ptr)`);
|
||||
const bytes = (text: string): string => `[${text.length + 1} x i8] c"${text}\\00"`;
|
||||
const lines = [
|
||||
`${LEVEL} = internal global i32 1`,
|
||||
`@sc_cpu_level_env = private unnamed_addr constant ${bytes("SCRIPTC_CPU_LEVEL")}`,
|
||||
...LEVEL_NAMES.map(
|
||||
([name], i) => `@sc_cpu_level_name_${i} = private unnamed_addr constant ${bytes(name)}`,
|
||||
),
|
||||
`define internal void @sc_cpu_level_init() #0 {`,
|
||||
`entry:`,
|
||||
` %max = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 0, i32 0)`,
|
||||
` %max.leaf = extractvalue { i32, i32, i32, i32 } %max, 0`,
|
||||
` %has.leaf7 = icmp uge i32 %max.leaf, 7`,
|
||||
` br i1 %has.leaf7, label %leaf1, label %detected`,
|
||||
`leaf1:`,
|
||||
` %l1 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 1, i32 0)`,
|
||||
` %l1.ecx = extractvalue { i32, i32, i32, i32 } %l1, 2`,
|
||||
` %l1.bits = and i32 %l1.ecx, ${LEAF1_ECX}`,
|
||||
` %l1.ok = icmp eq i32 %l1.bits, ${LEAF1_ECX}`,
|
||||
` br i1 %l1.ok, label %leaf7, label %detected`,
|
||||
`leaf7:`,
|
||||
` %l7 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 7, i32 0)`,
|
||||
` %l7.ebx = extractvalue { i32, i32, i32, i32 } %l7, 1`,
|
||||
` %l7.bits = and i32 %l7.ebx, ${LEAF7_EBX}`,
|
||||
` %l7.ok = icmp eq i32 %l7.bits, ${LEAF7_EBX}`,
|
||||
` %ext = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 -2147483648, i32 0)`,
|
||||
` %ext.max = extractvalue { i32, i32, i32, i32 } %ext, 0`,
|
||||
` %ext.has = icmp uge i32 %ext.max, -2147483647`,
|
||||
` %l7.ext = and i1 %l7.ok, %ext.has`,
|
||||
` br i1 %l7.ext, label %ext1, label %detected`,
|
||||
`ext1:`,
|
||||
` %e1 = call { i32, i32, i32, i32 } asm "cpuid", "={ax},={bx},={cx},={dx},{ax},{cx}"(i32 -2147483647, i32 0)`,
|
||||
` %e1.ecx = extractvalue { i32, i32, i32, i32 } %e1, 2`,
|
||||
` %e1.bits = and i32 %e1.ecx, ${EXT1_ECX}`,
|
||||
` %e1.ok = icmp eq i32 %e1.bits, ${EXT1_ECX}`,
|
||||
` br i1 %e1.ok, label %xcr0, label %detected`,
|
||||
// Leaf 1 reported OSXSAVE, so XGETBV is available.
|
||||
`xcr0:`,
|
||||
` %xcr = call { i32, i32 } asm "xgetbv", "={ax},={dx},{cx}"(i32 0)`,
|
||||
` %xcr.lo = extractvalue { i32, i32 } %xcr, 0`,
|
||||
` %xcr.bits = and i32 %xcr.lo, 6`,
|
||||
` %xcr.ok = icmp eq i32 %xcr.bits, 6`,
|
||||
` %v3 = select i1 %xcr.ok, i32 3, i32 1`,
|
||||
` br label %detected`,
|
||||
`detected:`,
|
||||
` %level = phi i32 [ 1, %entry ], [ 1, %leaf1 ], [ 1, %leaf7 ], [ 1, %ext1 ], [ %v3, %xcr0 ]`,
|
||||
` %env = call ptr @getenv(ptr @sc_cpu_level_env)`,
|
||||
` %has.env = icmp ne ptr %env, null`,
|
||||
` br i1 %has.env, label %cap, label %store`,
|
||||
`cap:`,
|
||||
];
|
||||
// The matching name's level, or 0 when none matches.
|
||||
let cap = "0";
|
||||
LEVEL_NAMES.forEach(([, level], i) => {
|
||||
lines.push(
|
||||
` %name.${i} = call i32 @strcmp(ptr %env, ptr @sc_cpu_level_name_${i})`,
|
||||
` %is.${i} = icmp eq i32 %name.${i}, 0`,
|
||||
` %cap.${i} = select i1 %is.${i}, i32 ${level}, i32 ${cap}`,
|
||||
);
|
||||
cap = `%cap.${i}`;
|
||||
});
|
||||
lines.push(
|
||||
` %capped.below = icmp ult i32 ${cap}, %level`,
|
||||
` %capped = select i1 %capped.below, i32 ${cap}, i32 %level`,
|
||||
` br label %store`,
|
||||
`store:`,
|
||||
` %final = phi i32 [ %level, %detected ], [ %capped, %cap ]`,
|
||||
` store i32 %final, ptr ${LEVEL}`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The call main makes before anything else. */
|
||||
export const CPU_LEVEL_INIT_CALL = ` call void @sc_cpu_level_init()`;
|
||||
|
||||
/** A function definition's header, split around its name and parameters. */
|
||||
interface Header {
|
||||
/** `define` and the linkage, up to the return type. */
|
||||
linkage: string;
|
||||
/** The return type with its attributes. */
|
||||
ret: string;
|
||||
name: string;
|
||||
/** The parameter list between the parentheses. */
|
||||
params: string;
|
||||
/** Function attributes before the group (`noinline`, `inlinehint`). */
|
||||
attrs: string;
|
||||
group: string;
|
||||
/** What follows the group: a personality, then `{` and a comment. */
|
||||
rest: string;
|
||||
}
|
||||
|
||||
const HEADER_START = /^define ((?:internal |private )?)(.*?)@([\w.$-]+)\(/;
|
||||
const HEADER_END = /^((?: [a-z_]+)*) #(\d+)( .*)$/;
|
||||
const RETURN_ATTRIBUTES =
|
||||
/\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g;
|
||||
|
||||
function parseHeader(line: string): Header | null {
|
||||
const start = HEADER_START.exec(line);
|
||||
if (start === null) return null;
|
||||
const open = start[0].length - 1;
|
||||
let depth = 0;
|
||||
let close = open;
|
||||
for (; close < line.length; close++) {
|
||||
const c = line.charAt(close);
|
||||
if (c === "(") depth++;
|
||||
else if (c === ")" && --depth === 0) break;
|
||||
}
|
||||
const end = HEADER_END.exec(line.slice(close + 1));
|
||||
if (end === null) return null;
|
||||
return {
|
||||
linkage: `define ${start[1]!}`,
|
||||
ret: start[2]!.trim(),
|
||||
name: start[3]!,
|
||||
params: line.slice(open + 1, close),
|
||||
attrs: end[1]!,
|
||||
group: end[2]!,
|
||||
rest: end[3]!,
|
||||
};
|
||||
}
|
||||
|
||||
/** The parameters split at their top-level commas. */
|
||||
function splitParams(params: string): string[] {
|
||||
const out: string[] = [];
|
||||
let depth = 0;
|
||||
let from = 0;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const c = params.charAt(i);
|
||||
if (c === "(" || c === "{" || c === "[" || c === "<") depth++;
|
||||
else if (c === ")" || c === "}" || c === "]" || c === ">") depth--;
|
||||
else if (c === "," && depth === 0) {
|
||||
out.push(params.slice(from, i).trim());
|
||||
from = i + 1;
|
||||
}
|
||||
}
|
||||
if (params.trim() !== "") out.push(params.slice(from).trim());
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A definition the pass copies: one in an attribute group it has a copy
|
||||
* group for (coroutine bodies, #1, keep their one body), with named
|
||||
* parameters, no variable arguments and no debug locations. */
|
||||
function cloneable(header: Header, text: string): boolean {
|
||||
if (!COPY_GROUPS.has(header.group)) return false;
|
||||
if (text.includes("!dbg")) return false;
|
||||
return splitParams(header.params).every((param) => /%[\w.$-]+$/.test(param));
|
||||
}
|
||||
|
||||
/** The dispatching definition under the function's own name: it tail-calls
|
||||
* the x86-64-v3 copy at level 3 or higher and the baseline copy otherwise. */
|
||||
function dispatcher(header: Header): string {
|
||||
const args = splitParams(header.params).join(", ");
|
||||
const type = header.ret.replace(RETURN_ATTRIBUTES, "").trim();
|
||||
const call = (tier: string, result: string): string[] =>
|
||||
type === "void"
|
||||
? [` musttail call void @${header.name}.${tier}(${args})`, ` ret void`]
|
||||
: [
|
||||
` ${result} = musttail call ${header.ret} @${header.name}.${tier}(${args})`,
|
||||
` ret ${type} ${result}`,
|
||||
];
|
||||
return [
|
||||
`${header.linkage}${header.ret} @${header.name}(${header.params}) #0 {`,
|
||||
`entry:`,
|
||||
` %sc.cpu.level = load i32, ptr ${LEVEL}`,
|
||||
` %sc.cpu.v3 = icmp uge i32 %sc.cpu.level, 3`,
|
||||
` br i1 %sc.cpu.v3, label %sc.cpu.v3.call, label %sc.cpu.base.call`,
|
||||
`sc.cpu.v3.call:`,
|
||||
...call("v3", "%sc.cpu.v3.result"),
|
||||
`sc.cpu.base.call:`,
|
||||
...call("base", "%sc.cpu.base.result"),
|
||||
`}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** A definition split at its header line and its closing brace. */
|
||||
interface Definition {
|
||||
header: Header;
|
||||
/** The lines after the header, through the closing `}`. */
|
||||
body: string;
|
||||
/** Text after the closing brace, kept as it is. */
|
||||
after: string;
|
||||
}
|
||||
|
||||
/** Rewrites the program's function definitions in `parts` for runtime
|
||||
* dispatch. A definition is a part, or a `define` chunk of a part, that
|
||||
* follows a partition group line (emit-module.ts partitionGroupLines);
|
||||
* runtime helpers and the module's entry points stay as they are. Each
|
||||
* cloneable function `@f` becomes:
|
||||
* - `@f.base`, its body as before;
|
||||
* - `@f.v3`, the same body in an x86-64-v3 attribute group;
|
||||
* - `@f`, a dispatcher with the same signature, which every reference to
|
||||
* the function as a value and every call from outside the program's
|
||||
* functions still names; a function with no such reference gets none.
|
||||
* Direct calls between cloned functions go to the caller's own tier, so the
|
||||
* level is tested once on entry into the program's code. */
|
||||
export function cloneForCpuDispatch(parts: readonly string[]): string[] {
|
||||
const program: { index: number; chunks: string[] }[] = [];
|
||||
for (let i = 1; i < parts.length; i++)
|
||||
if (parts[i - 1]!.startsWith("; scriptc-group") && parts[i]!.startsWith("define "))
|
||||
program.push({ index: i, chunks: parts[i]!.split(/\n(?=define )/) });
|
||||
const defs = new Map<string, Definition>();
|
||||
for (const { chunks } of program)
|
||||
for (const chunk of chunks) {
|
||||
const newline = chunk.indexOf("\n");
|
||||
const header = newline < 0 ? null : parseHeader(chunk.slice(0, newline));
|
||||
if (header === null) continue;
|
||||
const close = chunk.indexOf("\n}", newline);
|
||||
if (close < 0) continue;
|
||||
const body = chunk.slice(newline + 1, close + 2);
|
||||
if (!cloneable(header, body)) continue;
|
||||
defs.set(header.name, { header, body, after: chunk.slice(close + 2) });
|
||||
}
|
||||
if (defs.size === 0) return [...parts];
|
||||
const callee = /@([\w.$-]+)\(/g;
|
||||
const retarget = (body: string, tier: string): string =>
|
||||
body.replace(callee, (all, name: string) => (defs.has(name) ? `@${name}.${tier}(` : all));
|
||||
const named = namedFunctions(parts, program, defs, (body) => retarget(body, "base"));
|
||||
const out = [...parts];
|
||||
for (const { index, chunks } of program)
|
||||
out[index] = chunks
|
||||
.map((chunk) => {
|
||||
const header = HEADER_START.exec(chunk);
|
||||
const def = header === null ? undefined : defs.get(header[3]!);
|
||||
if (def === undefined) return chunk;
|
||||
const h = def.header;
|
||||
const copy = (tier: string, group: string): string =>
|
||||
`define internal ${h.ret} @${h.name}.${tier}(${h.params})${h.attrs} #${group}${h.rest}\n${retarget(def.body, tier)}`;
|
||||
return (
|
||||
[
|
||||
copy("base", h.group),
|
||||
copy("v3", COPY_GROUPS.get(h.group)!),
|
||||
...(named.has(h.name) ? [dispatcher(h)] : []),
|
||||
].join("\n") + def.after
|
||||
);
|
||||
})
|
||||
.join("\n");
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The cloned functions whose own name the module still uses once the
|
||||
* copies call each other: as a value, or in a call from code that is not
|
||||
* copied (main, runtime helpers, coroutine bodies). Only these get a
|
||||
* dispatcher. A function without one keeps the callers it had, so LLVM
|
||||
* inlines and removes its copies as it would the original. */
|
||||
function namedFunctions(
|
||||
parts: readonly string[],
|
||||
program: readonly { index: number; chunks: string[] }[],
|
||||
defs: ReadonlyMap<string, Definition>,
|
||||
retargeted: (body: string) => string,
|
||||
): Set<string> {
|
||||
const named = new Set<string>();
|
||||
const scan = (text: string): void => {
|
||||
for (const m of text.matchAll(/@([\w.$-]+)/g)) if (defs.has(m[1]!)) named.add(m[1]!);
|
||||
};
|
||||
const inProgram = new Set(program.map(({ index }) => index));
|
||||
parts.forEach((part, i) => {
|
||||
if (!inProgram.has(i)) scan(part);
|
||||
});
|
||||
for (const { chunks } of program)
|
||||
for (const chunk of chunks) {
|
||||
const header = HEADER_START.exec(chunk);
|
||||
const def = header === null ? undefined : defs.get(header[3]!);
|
||||
if (def === undefined) scan(chunk);
|
||||
else scan(retargeted(def.body) + def.after);
|
||||
}
|
||||
return named;
|
||||
}
|
||||
@@ -13,9 +13,37 @@ import {
|
||||
import { SourceLocations } from "../source-locations.js";
|
||||
import { mangleFunction } from "../mangle.js";
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations. */
|
||||
/** A request a range emitter made of its debug info that added nodes
|
||||
* (LlvmDebugInfo.record), with the ids it answered. */
|
||||
export type DebugRequest =
|
||||
| { readonly op: "function"; readonly fn: string; readonly result: string }
|
||||
| {
|
||||
readonly op: "local";
|
||||
readonly local: IrLocal;
|
||||
readonly fn: string;
|
||||
readonly arg: number;
|
||||
readonly captured: boolean;
|
||||
readonly result: { variable: string; location: string };
|
||||
}
|
||||
| {
|
||||
readonly op: "location";
|
||||
readonly loc: SrcLoc;
|
||||
readonly fn: string;
|
||||
readonly result: string;
|
||||
};
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations.
|
||||
*
|
||||
* Nodes are numbered in request order. A range emitter (emit-module.ts)
|
||||
* records the requests that added nodes; the emitter that merges the range
|
||||
* replays them in order, so every node gets the number serial emission
|
||||
* gives it, and renames the range's text from the ids the range answered to
|
||||
* its own. A request that added no node answered an id that existed before
|
||||
* the range started or that an earlier recorded request created. */
|
||||
export class LlvmDebugInfo {
|
||||
private readonly nodes: string[] = [];
|
||||
/** The requests that added nodes, while recording (record). */
|
||||
private requests: DebugRequest[] | null = null;
|
||||
private readonly files = new Map<string, string>();
|
||||
private readonly locations = new Map<string, string>();
|
||||
private readonly scopes = new Map<string, string>();
|
||||
@@ -73,7 +101,53 @@ export class LlvmDebugInfo {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/** Records the requests that add nodes from here on. */
|
||||
record(): void {
|
||||
this.requests = [];
|
||||
}
|
||||
|
||||
/** The requests recorded since record. */
|
||||
recorded(): readonly DebugRequest[] {
|
||||
return this.requests ?? [];
|
||||
}
|
||||
|
||||
/** Replays a range's recorded requests in order and adds to `ids` the
|
||||
* rename from the ids the range answered to this debug info's. */
|
||||
replay(
|
||||
requests: readonly DebugRequest[],
|
||||
functionNamed: (name: string) => IrFunction,
|
||||
ids: Map<string, string>,
|
||||
): void {
|
||||
const own = (id: string, to: string | null | undefined): void => {
|
||||
if (to === null || to === undefined)
|
||||
throw new Error(`llvm emitter bug: debug request for ${id} added no node on replay`);
|
||||
if (to !== id) ids.set(id, to);
|
||||
};
|
||||
for (const request of requests) {
|
||||
if (request.op === "function") {
|
||||
own(request.result, this.function(functionNamed(request.fn)));
|
||||
continue;
|
||||
}
|
||||
const fn = ids.get(request.fn) ?? request.fn;
|
||||
if (request.op === "location") {
|
||||
own(request.result, this.location(request.loc, fn));
|
||||
continue;
|
||||
}
|
||||
const local = this.local(request.local, fn, request.arg, request.captured);
|
||||
own(request.result.variable, local?.variable);
|
||||
own(request.result.location, local?.location);
|
||||
}
|
||||
}
|
||||
|
||||
function(fn: IrFunction): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.functionNode(fn);
|
||||
if (this.requests !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "function", fn: fn.name, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private functionNode(fn: IrFunction): string | null {
|
||||
const pos = this.source.position(fn.loc);
|
||||
if (pos === null) return null;
|
||||
const file = this.file(pos.file);
|
||||
@@ -230,6 +304,26 @@ export class LlvmDebugInfo {
|
||||
fn: string | null,
|
||||
arg = 0,
|
||||
captured = false,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
const before = this.nodes.length;
|
||||
const result = this.localNodes(local, fn, arg, captured);
|
||||
if (this.requests !== null && fn !== null && result !== null && this.nodes.length > before)
|
||||
this.requests.push({
|
||||
op: "local",
|
||||
local,
|
||||
fn,
|
||||
arg,
|
||||
captured,
|
||||
result: { variable: result.variable, location: result.location },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private localNodes(
|
||||
local: IrLocal,
|
||||
fn: string | null,
|
||||
arg: number,
|
||||
captured: boolean,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
if (fn === null || local.source === undefined) return null;
|
||||
const pos = this.source.position(local.source.loc);
|
||||
@@ -279,6 +373,14 @@ export class LlvmDebugInfo {
|
||||
}
|
||||
|
||||
location(loc: SrcLoc, fn: string | null): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.locationNode(loc, fn);
|
||||
if (this.requests !== null && fn !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "location", loc, fn, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private locationNode(loc: SrcLoc, fn: string | null): string | null {
|
||||
if (fn === null) return null;
|
||||
const pos = this.source.position(loc);
|
||||
if (pos === null) return null;
|
||||
|
||||
@@ -453,7 +453,11 @@ export function emitDenseArrayRead(
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
const slowValue = B.tmp();
|
||||
const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK;
|
||||
// ToNumber of a foreign element (SCR_ARR_FOREIGN) can run user code.
|
||||
const attrs =
|
||||
(acc === "ref" && retain) || (getter === "scr_arr_get_number" && host.mayHoldForeign(element))
|
||||
? " cold"
|
||||
: READ_FALLBACK;
|
||||
B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`);
|
||||
let slowInt: string | null = null;
|
||||
if (fastInt !== null) {
|
||||
@@ -610,6 +614,28 @@ export function emitDenseArraySet(
|
||||
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
}
|
||||
};
|
||||
if ((acc === "f64" || kind === "ref") && host.mayHoldForeign(element)) {
|
||||
// Past the hole-free prefix a slot may hold a foreign element, whose
|
||||
// value the runtime setter releases; below it every slot is a value.
|
||||
B.br(slow);
|
||||
B.startBlock(filledLabel);
|
||||
const filledValuePtr = valuePointer(host, guard, kind);
|
||||
let filledOld: string | null = null;
|
||||
if (kind === "ref") {
|
||||
filledOld = B.tmp();
|
||||
B.line(
|
||||
`${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`,
|
||||
);
|
||||
}
|
||||
storeValue(filledValuePtr);
|
||||
if (filledOld) host.releaseHeap(filledOld, element);
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
call(" cold");
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
return;
|
||||
}
|
||||
const stated = B.newLabel("arr.set.states"),
|
||||
plain = B.newLabel("arr.set.plain"),
|
||||
plainStore = B.newLabel("arr.set.values"),
|
||||
|
||||
@@ -66,6 +66,25 @@ import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js
|
||||
import type { WalkerHost } from "./walkers.js";
|
||||
import type { LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
|
||||
/** typeKey with `?` after every `(T | undefined)[]` (optionalElements),
|
||||
* at any depth of arrays, Maps, Sets and promises: checked conversions to
|
||||
* `number[][]` and `(number | undefined)[][]` need separate helpers, though
|
||||
* the two types share a typeKey. */
|
||||
function checkedConversionKey(t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "array":
|
||||
return `array<${checkedConversionKey(t.elem)}>${t.optionalElements ? "?" : ""}`;
|
||||
case "map":
|
||||
return `map<${checkedConversionKey(t.key)},${checkedConversionKey(t.value)}>`;
|
||||
case "set":
|
||||
return `set<${checkedConversionKey(t.elem)}>`;
|
||||
case "promise":
|
||||
return `promise<${checkedConversionKey(t.inner)}>`;
|
||||
default:
|
||||
return typeKey(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether to-dyn of a record shape keeps the record itself: an empty shape
|
||||
* and an event-listener record (a `handleEvent` function field) box their
|
||||
* identity. Every other shape, tuples included, copies its fields into a
|
||||
@@ -759,6 +778,13 @@ export class LlDyn {
|
||||
/* ── dyn node plumbing ─────────────────────────────────────────────── */
|
||||
|
||||
/** Loads a dyn node's kind tag (after the size_t rc word). */
|
||||
/** i1: the checked-dynamic value `d` is undefined. */
|
||||
isUndefined(B: BlockBuilder, d: string): string {
|
||||
const is = B.tmp();
|
||||
B.line(`${is} = icmp eq i32 ${this.kindOf(B, d)}, ${DYN_KIND.UNDEF}`);
|
||||
return is;
|
||||
}
|
||||
|
||||
private kindOf(B: BlockBuilder, d: string): string {
|
||||
const p = B.tmp();
|
||||
const k = B.tmp();
|
||||
@@ -1145,7 +1171,7 @@ export class LlDyn {
|
||||
|
||||
/** `sc_dm_<n>(ptr d) -> i1` — does this dyn fit T? Never throws. */
|
||||
dynMatchHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
const key = checkedConversionKey(t);
|
||||
const matcherKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynMatchers.get(matcherKey);
|
||||
if (existing) return existing;
|
||||
@@ -1460,7 +1486,7 @@ export class LlDyn {
|
||||
B.startBlock(l0);
|
||||
const n = this.lenOf(B, "%d");
|
||||
const items = this.itemsOf(B, "%d");
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext) => {
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext, next) => {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
||||
@@ -1471,6 +1497,15 @@ export class LlDyn {
|
||||
brNext();
|
||||
B.startBlock(valueLabel);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// A `(T | undefined)[]` stored as T elements takes undefined
|
||||
// elements as well.
|
||||
const undef = B.tmp();
|
||||
const checkLabel = B.newLabel("dm.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, next, checkLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = call zeroext i1 @${m}(ptr ${e})`);
|
||||
const lOk = B.newLabel("dm.eo");
|
||||
@@ -1945,8 +1980,90 @@ export class LlDyn {
|
||||
/** `sc_dc_<n>(ptr d, ptr path) -> T` — validate the checked-dynamic tree against T and
|
||||
* BUILD the typed value (+1), or throw the catchable path-annotated
|
||||
* TypeError and return a dummy with the pending flag set. */
|
||||
/** The array a live view's snapshot commits back into, for element types
|
||||
* whose arrays can hold foreign elements (LlEmitter.mayHoldForeign):
|
||||
* like dynCheckHelper's array case on a checked array `%d`, except that
|
||||
* an element the element type cannot hold is stored as a foreign element
|
||||
* (undefined as a present undefined) instead of failing the conversion.
|
||||
* Boolean arrays, whose slots cannot hold one, keep the failing check. */
|
||||
dynViewArrayHelper(t: IrType & { kind: "array" }): string {
|
||||
const builderKey = `view:${typeKey(t)}`;
|
||||
const existing = this.dynBuilders.get(builderKey);
|
||||
if (existing) return existing;
|
||||
const name = `sc_dva_${this.dynBuilders.size}`;
|
||||
this.dynBuilders.set(builderKey, name);
|
||||
const host = this.host;
|
||||
const elem = t.elem;
|
||||
const check = this.dynCheckHelper(elem);
|
||||
const match = this.dynMatchHelper(elem);
|
||||
const B = new BlockBuilder();
|
||||
const n = this.lenOf(B, "%d");
|
||||
const a = B.tmp();
|
||||
B.line(`${a} = ${arrNewCall(host, elem, n)}`);
|
||||
const items = this.itemsOf(B, "%d");
|
||||
const acc = elemAccess(elem);
|
||||
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
host.declare(
|
||||
`declare double @scr_arr_push_${acc}(ptr, ${acc === "bool" ? "i1 zeroext" : accTy})`,
|
||||
);
|
||||
host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
||||
host.declare(`declare void @scr_arr_set_len(ptr, double)`);
|
||||
host.declare(`declare zeroext i1 @scr_arr_set_foreign(ptr, double, ptr)`);
|
||||
this.i64Loop(B, "dva", n, (i) => {
|
||||
const index = B.tmp();
|
||||
B.line(`${index} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
||||
const valueLabel = B.newLabel("dva.present");
|
||||
const holeLabel = B.newLabel("dva.hole");
|
||||
const typedLabel = B.newLabel("dva.typed");
|
||||
const foreignLabel = B.newLabel("dva.foreign");
|
||||
const doneLabel = B.newLabel("dva.done");
|
||||
B.condBr(present, valueLabel, holeLabel);
|
||||
B.startBlock(holeLabel);
|
||||
const length = B.tmp();
|
||||
B.line(`${length} = fadd double ${index}, 1.0`);
|
||||
B.line(`call void @scr_arr_set_len(ptr ${a}, double ${length})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(valueLabel);
|
||||
const e = this.itemAt(B, items, i);
|
||||
const fits = B.tmp();
|
||||
B.line(`${fits} = call zeroext i1 @${match}(ptr ${e})`);
|
||||
B.condBr(fits, typedLabel, foreignLabel);
|
||||
B.startBlock(foreignLabel);
|
||||
const kept = B.tmp();
|
||||
B.line(`${kept} = call zeroext i1 @scr_arr_set_foreign(ptr ${a}, double ${index}, ptr ${e})`);
|
||||
B.condBr(kept, doneLabel, typedLabel);
|
||||
B.startBlock(typedLabel);
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = call ${this.valTy(elem)} @${check}(ptr ${e}, ptr null)`);
|
||||
this.pendingBail(
|
||||
B,
|
||||
"dva",
|
||||
() => {
|
||||
host.declare(`declare void @scr_arr_release(ptr)`);
|
||||
B.line(`call void @scr_arr_release(ptr ${a})`);
|
||||
},
|
||||
"ptr null",
|
||||
);
|
||||
const pushed = B.tmp();
|
||||
B.line(`${pushed} = call double @scr_arr_push_${acc}(ptr ${a}, ${accTy} ${v})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(doneLabel);
|
||||
});
|
||||
B.terminate(`ret ptr ${a}`);
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} { ; view commit ${typeKey(t)}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return name;
|
||||
}
|
||||
|
||||
dynCheckHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
// A `(T | undefined)[]` stored as T elements also takes undefined ones.
|
||||
const key = checkedConversionKey(t);
|
||||
const builderKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynBuilders.get(builderKey);
|
||||
if (existing) return existing;
|
||||
@@ -2753,6 +2870,21 @@ export class LlDyn {
|
||||
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
||||
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// An undefined element is a present undefined element.
|
||||
const undef = B.tmp();
|
||||
const storeUndefined = B.newLabel("dca.undef");
|
||||
const checkLabel = B.newLabel("dca.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, storeUndefined, checkLabel);
|
||||
B.startBlock(storeUndefined);
|
||||
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
||||
const at = B.tmp();
|
||||
B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
||||
B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`);
|
||||
this.pendingBail(
|
||||
@@ -3635,7 +3767,25 @@ export class LlDyn {
|
||||
B.br(doneLabel);
|
||||
B.startBlock(presentLabel);
|
||||
B.line(`${undefinedState} = fcmp oeq double ${state}, 2.0`); // SCR_ARR_UNDEFINED
|
||||
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${valueLabel}`);
|
||||
if (host.mayHoldForeign(t.elem)) {
|
||||
// A foreign element (SCR_ARR_FOREIGN) holds its own value.
|
||||
const typedLabel = B.newLabel("tda.typed");
|
||||
const foreignLabel = B.newLabel("tda.foreign");
|
||||
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${typedLabel}`);
|
||||
B.startBlock(typedLabel);
|
||||
const foreign = B.tmp();
|
||||
B.line(`${foreign} = fcmp oeq double ${state}, 3.0`);
|
||||
B.condBr(foreign, foreignLabel, valueLabel);
|
||||
B.startBlock(foreignLabel);
|
||||
host.declare(`declare ptr @scr_arr_get_foreign(ptr, double)`);
|
||||
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
||||
const payload = B.tmp();
|
||||
B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %v, double ${i}) ; +1`);
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${payload})`);
|
||||
B.br(doneLabel);
|
||||
} else {
|
||||
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${valueLabel}`);
|
||||
}
|
||||
B.startBlock(undefinedLabel);
|
||||
const retained = this.retainDyn(B, this.undef(B));
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${retained})`);
|
||||
|
||||
@@ -83,6 +83,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.unwindCleanups.clear();
|
||||
emitter.jumpTargets = [];
|
||||
emitter.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
emitter.currentParameters = new Set(fn.params.map((p) => p.localId));
|
||||
emitter.lazyCaptureBoxes.clear();
|
||||
emitter.lazyStackBoxes.clear();
|
||||
// An unchanged captured parameter keeps its value in a plain slot; the
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* replay calls the same entry point with the same arguments, so every
|
||||
* helper gets the number and the definition the serial emission gives it,
|
||||
* and the range's text is renamed to those numbers. */
|
||||
import type { DebugRequest } from "./debug-info.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
|
||||
/** One event of a range's emission, in order. */
|
||||
@@ -97,6 +98,8 @@ export class EmitJournal {
|
||||
* worker process can write it. */
|
||||
export interface ChunkResult {
|
||||
readonly entries: readonly JournalEntry[];
|
||||
/** The requests that added debug metadata nodes (LlvmDebugInfo.record). */
|
||||
readonly debugRequests: readonly DebugRequest[];
|
||||
readonly flags: {
|
||||
readonly usesCoroutines: boolean;
|
||||
readonly usesTimers: boolean;
|
||||
@@ -152,23 +155,83 @@ export function replayJournal(
|
||||
}
|
||||
|
||||
/** The rename of a range's text: numbered names (`names`, without the
|
||||
* sigil) and field alias tags (`tags`). One scan for each kind of name:
|
||||
* a numbered name starts `sc_` after a character that cannot be part of a
|
||||
* name, and ends at the last digit after its counter prefix. */
|
||||
* sigil), field alias tags (`tags`) and debug metadata ids (`debugIds`,
|
||||
* `!N`). One scan for each kind of name: a numbered name starts `sc_`
|
||||
* after a character that cannot be part of a name, and ends at the last
|
||||
* digit after its counter prefix. */
|
||||
export function renameChunk(
|
||||
names: ReadonlyMap<string, string>,
|
||||
tags: ReadonlyMap<number, number>,
|
||||
debugIds: ReadonlyMap<string, string> = new Map(),
|
||||
): (text: string) => string {
|
||||
const prefixes = new Set<string>();
|
||||
for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, ""));
|
||||
if (prefixes.size === 0 && tags.size === 0) return (text) => text;
|
||||
if (prefixes.size === 0 && tags.size === 0 && debugIds.size === 0) return (text) => text;
|
||||
return (text) => {
|
||||
let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes);
|
||||
if (tags.size > 0) out = renameTags(out, tags);
|
||||
if (debugIds.size > 0) out = renameDebugIds(out, debugIds);
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
/** Renames metadata references `!N` outside string literals and comments.
|
||||
* Debug builds attach no alias tags, so every such reference in a
|
||||
* function's text is a debug metadata id. A string literal or a comment
|
||||
* (`;` outside a string literal) ends at the end of its line; a quote
|
||||
* inside a string literal is escaped (`\22`). */
|
||||
function renameDebugIds(text: string, ids: ReadonlyMap<string, string>): string {
|
||||
const parts: string[] = [];
|
||||
let copied = 0;
|
||||
// The line of the reference being looked at, and whether the reference
|
||||
// is inside a string literal or a comment there. The next quote and
|
||||
// semicolon positions only move forward, so each is found once.
|
||||
let lineEnd = -1;
|
||||
let quoted = false;
|
||||
let comment = false;
|
||||
let quote = text.indexOf('"');
|
||||
let semicolon = text.indexOf(";");
|
||||
for (let at = text.indexOf("!"); at >= 0; at = text.indexOf("!", at + 1)) {
|
||||
if (at > lineEnd) {
|
||||
lineEnd = text.indexOf("\n", at);
|
||||
if (lineEnd < 0) lineEnd = text.length;
|
||||
quoted = false;
|
||||
comment = false;
|
||||
// Quotes and semicolons of earlier lines end with them.
|
||||
if ((quote >= 0 && quote < at) || (semicolon >= 0 && semicolon < at)) {
|
||||
const lineStart = text.lastIndexOf("\n", at) + 1;
|
||||
while (quote >= 0 && quote < lineStart) quote = text.indexOf('"', quote + 1);
|
||||
while (semicolon >= 0 && semicolon < lineStart)
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
while (!comment) {
|
||||
const nextQuote = quote >= 0 && quote < at ? quote : -1;
|
||||
const nextSemicolon = semicolon >= 0 && semicolon < at ? semicolon : -1;
|
||||
if (nextQuote < 0 && nextSemicolon < 0) break;
|
||||
if (nextQuote >= 0 && (nextSemicolon < 0 || nextQuote < nextSemicolon)) {
|
||||
quoted = !quoted;
|
||||
quote = text.indexOf('"', quote + 1);
|
||||
} else {
|
||||
if (!quoted) comment = true;
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
if (quoted || comment) continue;
|
||||
if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue;
|
||||
let end = at + 1;
|
||||
while (end < text.length && isDigit(text.charCodeAt(end))) end++;
|
||||
if (end === at + 1 || (end < text.length && isNameChar(text.charCodeAt(end)))) continue;
|
||||
const own = ids.get(text.slice(at, end));
|
||||
if (own === undefined) continue;
|
||||
parts.push(text.slice(copied, at), own);
|
||||
copied = end;
|
||||
}
|
||||
if (copied === 0) return text;
|
||||
parts.push(text.slice(copied));
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function renameNames(
|
||||
text: string,
|
||||
names: ReadonlyMap<string, string>,
|
||||
|
||||
@@ -65,7 +65,9 @@ import { STRING_READ_HELPERS, stringScanDecl, stringScanInline } from "./string-
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { targetCpuAttribute } from "../target-cpu.js";
|
||||
import { CPU_LEVEL_INIT_CALL, cpuDispatchAttributes, cpuLevelDefs } from "./cpu-dispatch.js";
|
||||
import type { IrFunction } from "../../ir/ir.js";
|
||||
import { type FunctionLayout, layoutTierOf } from "./function-layout.js";
|
||||
|
||||
/** The function bodies emitted in `chunks` contiguous ranges, each by its
|
||||
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
|
||||
@@ -134,6 +136,7 @@ function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] |
|
||||
let code = 1;
|
||||
try {
|
||||
emitter.journal = new EmitJournal();
|
||||
emitter.debug?.record();
|
||||
const texts: string[] = [];
|
||||
for (const fn of functions.slice(range * size, (range + 1) * size)) {
|
||||
texts.push(emitter.emitFunction(fn));
|
||||
@@ -195,15 +198,19 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st
|
||||
|
||||
/** The attribute groups of the emitted functions: #0 for every function,
|
||||
* #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
|
||||
* the build's `--target-cpu` level. */
|
||||
* the build's `--target-cpu` level, and `sanitize_address` in sanitized
|
||||
* builds. A group needs one attribute: without either, #0 holds a string
|
||||
* attribute that LLVM ignores. Dispatching builds add the x86-64-v3 copies'
|
||||
* #3 and #4 (cpu-dispatch.ts). */
|
||||
function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
|
||||
const cpu = targetCpuAttribute(emitter.targetCpu);
|
||||
const asan = emitter.sanitizeAttributes ? " sanitize_address" : "";
|
||||
const base = `${asan}${cpu}` || ` "scriptc-function"`;
|
||||
return [
|
||||
`attributes #0 = { sanitize_address${cpu} }`,
|
||||
...(emitter.usesCoroutines
|
||||
? [`attributes #1 = { sanitize_address presplitcoroutine${cpu} }`]
|
||||
: []),
|
||||
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address${cpu} }`] : []),
|
||||
`attributes #0 = {${base} }`,
|
||||
...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []),
|
||||
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []),
|
||||
...(emitter.cpuDispatch ? cpuDispatchAttributes(asan) : []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -227,16 +234,68 @@ function partitionGroupLines(functions: readonly IrFunction[]): string[] {
|
||||
/** Ends the group of the definitions that follow. */
|
||||
const PARTITION_GROUP_END = "; scriptc-group";
|
||||
|
||||
/** Writes the generated helpers, then the function definitions with their
|
||||
* group lines. With a layout, the definitions it lists, helpers included,
|
||||
* move after the unlisted helpers and before the other functions, by tier,
|
||||
* each in its tier's group (`layout-<tier>`). The helper compiles each
|
||||
* partition's definitions in text order, so listed ones come first in their
|
||||
* partitions, and it places a group's definitions in the same partitions.
|
||||
* Everything else keeps its order and its source-file group. */
|
||||
function writeDefinitions(
|
||||
out: string[],
|
||||
layout: FunctionLayout | undefined,
|
||||
helperTexts: readonly (readonly string[])[],
|
||||
fnDefs: readonly string[],
|
||||
groups: readonly string[],
|
||||
): void {
|
||||
const tierLine = (tier: number) => `${PARTITION_GROUP_END} layout-${tier}`;
|
||||
const listed = new Map<number, string[]>();
|
||||
const listIn = (tier: number, lines: readonly string[]) => {
|
||||
const list = listed.get(tier) ?? [];
|
||||
if (!listed.has(tier)) listed.set(tier, list);
|
||||
for (const line of lines) list.push(line);
|
||||
};
|
||||
if (layout === undefined || layout.size === 0) {
|
||||
for (const texts of helperTexts) for (const text of texts) out.push(text);
|
||||
} else {
|
||||
// A helper's text can span several entries (one per line), so the
|
||||
// definitions are found in the joined text. A listed definition moves
|
||||
// through its closing brace; what follows it stays in place.
|
||||
const joined: string[] = [];
|
||||
for (const texts of helperTexts) for (const text of texts) joined.push(text);
|
||||
for (const part of joined.join("\n").split(/\n(?=define )/)) {
|
||||
const tier = part.startsWith("define ") ? layoutTierOf(layout, part) : undefined;
|
||||
const close = tier === undefined ? -1 : part.search(/\n}(\n|$)/);
|
||||
if (tier === undefined || close < 0) {
|
||||
out.push(part);
|
||||
continue;
|
||||
}
|
||||
listIn(tier, [tierLine(tier), part.slice(0, close + 2)]);
|
||||
const rest = part.slice(close + 3);
|
||||
if (rest !== "") out.push(rest);
|
||||
}
|
||||
}
|
||||
const unlisted: number[] = [];
|
||||
fnDefs.forEach((text, i) => {
|
||||
const tier = layout === undefined || layout.size === 0 ? undefined : layoutTierOf(layout, text);
|
||||
if (tier === undefined) unlisted.push(i);
|
||||
else listIn(tier, ["", tierLine(tier), text]);
|
||||
});
|
||||
for (const tier of [...listed.keys()].sort((a, b) => a - b))
|
||||
for (const line of listed.get(tier)!) out.push(line);
|
||||
for (const [k, i] of unlisted.entries()) {
|
||||
if (k > 0 || listed.size > 0) out.push("");
|
||||
out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!);
|
||||
}
|
||||
}
|
||||
|
||||
export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
// Debug metadata is numbered per emitter and not journaled, so a debug
|
||||
// build emits every body here.
|
||||
const ranges = emitter.debug === null;
|
||||
const chunks = emitter.targetOptions.functionChunks ?? 1;
|
||||
const fnDefs =
|
||||
(ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
(ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ??
|
||||
(chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ??
|
||||
[];
|
||||
if (fnDefs.length === 0)
|
||||
for (const fn of emitter.mod.functions) {
|
||||
@@ -585,6 +644,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
...(emitter.publishWalkersCache?.defs() ?? []),
|
||||
...emitter.publishedClassNamesDefs(),
|
||||
...(emitter.regions?.moduleDefs() ?? []),
|
||||
...(emitter.cpuDispatch ? cpuLevelDefs((line) => emitter.declare(line)) : []),
|
||||
];
|
||||
if (emitter.constantNumericTables.size > 0)
|
||||
emitter.declare(`declare double @scr_arr_get_number(ptr, double)`);
|
||||
@@ -602,6 +662,23 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
regexMatchers.set(key, code);
|
||||
for (const decl of code.decls) emitter.declare(decl);
|
||||
}
|
||||
// Replacers of global substitutions with a literal replacement; the call
|
||||
// sites register only patterns inside the compiled subset.
|
||||
const regexReplacers: RegexMatcherCode[] = [];
|
||||
for (const rr of emitter.regexReplacers.values()) {
|
||||
const code = compileRegexMatcher(
|
||||
rr.sym,
|
||||
rr.pattern,
|
||||
rr.flags,
|
||||
emitter.sizeType,
|
||||
!emitter.wasi,
|
||||
rr.replacement,
|
||||
);
|
||||
if (code === null)
|
||||
throw new InternalCompilerError(`llvm emitter bug: replacer ${rr.sym} did not compile`);
|
||||
regexReplacers.push(code);
|
||||
for (const decl of code.decls) emitter.declare(decl);
|
||||
}
|
||||
|
||||
const out: string[] = [
|
||||
`; Generated by scriptc (LLVM backend) from ${emitter.mod.sourceFile}. Do not edit.`,
|
||||
@@ -884,6 +961,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
}
|
||||
if (emitter.regexInstances.size > 0) out.push(``);
|
||||
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
|
||||
for (const code of regexReplacers) out.push(...code.lines, ``);
|
||||
for (const [, inst] of emitter.templateStringsInstances) {
|
||||
// One immortal ScrArr per tagged-template site: a data global of
|
||||
// interned cooked-string literals, and the ScrArr header over it
|
||||
@@ -926,21 +1004,24 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
);
|
||||
}
|
||||
if (globals.length > 0) out.push(``);
|
||||
for (const line of helpers) out.push(line);
|
||||
for (const line of ffiCallbacks.defs) out.push(line);
|
||||
for (const line of shapes.defs) out.push(line);
|
||||
for (const line of classShapes.defs) out.push(line);
|
||||
for (const line of classObjDefs) out.push(line);
|
||||
for (const line of emitter.walkers.defs) out.push(line);
|
||||
for (const line of emitter.dyn.defs) out.push(line);
|
||||
for (const line of wrappers) out.push(line);
|
||||
for (const line of asyncDefs) out.push(line);
|
||||
for (const line of emitter.resolveThunkDefs) out.push(line);
|
||||
const groups = partitionGroupLines(emitter.mod.functions);
|
||||
for (let i = 0; i < fnDefs.length; i++) {
|
||||
if (i > 0) out.push("");
|
||||
out.push(groups[i] ?? PARTITION_GROUP_END, fnDefs[i]!);
|
||||
}
|
||||
writeDefinitions(
|
||||
out,
|
||||
emitter.targetOptions.functionLayout,
|
||||
[
|
||||
helpers,
|
||||
ffiCallbacks.defs,
|
||||
shapes.defs,
|
||||
classShapes.defs,
|
||||
classObjDefs,
|
||||
emitter.walkers.defs,
|
||||
emitter.dyn.defs,
|
||||
wrappers,
|
||||
asyncDefs,
|
||||
emitter.resolveThunkDefs,
|
||||
],
|
||||
fnDefs,
|
||||
partitionGroupLines(emitter.mod.functions),
|
||||
);
|
||||
if (fnDefs.length === 0) out.push("");
|
||||
out.push(PARTITION_GROUP_END, "");
|
||||
// WebAssembly modules translated into this program, and the registry.
|
||||
@@ -1025,6 +1106,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
`define i32 @${emitter.mod.workers ? "sc_context_entry" : emitter.wasi ? "__main_argc_argv" : "main"}(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
...(emitter.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []),
|
||||
...(emitter.cpuDispatch && !emitter.mod.workers ? [CPU_LEVEL_INIT_CALL] : []),
|
||||
...(emitter.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []),
|
||||
` call void @scr_init()`,
|
||||
// Compressed fields hold addresses in the program-lifetime region, which
|
||||
@@ -1163,9 +1245,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
);
|
||||
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
|
||||
out.push(
|
||||
// sanitize_address is inert under the plain pipeline; the sanitized
|
||||
// lane's -fsanitize=address link activates instrumentation over the
|
||||
// emitted functions too (the runtime TUs get theirs from clang).
|
||||
// The sanitized lane's -fsanitize=address link instruments the
|
||||
// functions marked sanitize_address (the runtime TUs get theirs from
|
||||
// clang).
|
||||
...functionAttributes(emitter, hasNoInlineRecordClone),
|
||||
``,
|
||||
);
|
||||
@@ -1183,6 +1265,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
`}`,
|
||||
`define i32 @main(i32 %argc, ptr %argv) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
// Worker threads enter at sc_worker_entry and read the level main set.
|
||||
...(emitter.cpuDispatch ? [CPU_LEVEL_INIT_CALL] : []),
|
||||
` %result = call i32 @sc_context_entry(i32 %argc, ptr %argv)`,
|
||||
` call void @scr_context_cleanup()`,
|
||||
` ret i32 %result`,
|
||||
@@ -1194,6 +1278,126 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The out-of-line hashed probe of a number-key map (scr_map_find_f64):
|
||||
* `keyLines` define the normalized key word %kb from the parameter, and
|
||||
* `runtimeKey` is the double passed to the runtime lookup. A map with at
|
||||
* most SCR_MAP_LINEAR_LIMIT entries keeps no bucket table: the absent-key
|
||||
* filter is tested before its entries are scanned. A bucket table is
|
||||
* probed without the filter: the filter keeps two of its 64 bits per key,
|
||||
* so past a few dozen keys it rejects few absent keys, and the probe's
|
||||
* first bucket read answers those. Compact tables tag each 24-bit entry
|
||||
* index (+1) with the hash's top byte; wider tables take the runtime
|
||||
* lookup. */
|
||||
function hashedNumberProbe(
|
||||
name: string,
|
||||
param: string,
|
||||
sz: string,
|
||||
keyLines: string[],
|
||||
runtimeKey: string,
|
||||
): string[] {
|
||||
return [
|
||||
`define internal ptr @${name}(ptr %m, ${param}) noinline ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
...keyLines,
|
||||
` %x1s = lshr i64 %kb, 30`,
|
||||
` %x1 = xor i64 %kb, %x1s`,
|
||||
` %x2 = mul i64 %x1, -4658895280553007687`,
|
||||
` %x3s = lshr i64 %x2, 27`,
|
||||
` %x3 = xor i64 %x2, %x3s`,
|
||||
` %x4 = mul i64 %x3, -7723592293110705685`,
|
||||
` %x5s = lshr i64 %x4, 31`,
|
||||
` %x5 = xor i64 %x4, %x5s`,
|
||||
` %hz = icmp eq i64 %x5, 0`,
|
||||
` %h = select i1 %hz, i64 1, i64 %x5`,
|
||||
` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`,
|
||||
...(sz === "i64"
|
||||
? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`]
|
||||
: [` %nb = load i32, ptr %nbp`]),
|
||||
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %ents = load ptr, ptr %eps`,
|
||||
` %linear = icmp eq ${sz} %nb, 0`,
|
||||
` br i1 %linear, label %filter, label %buckets`,
|
||||
`filter:`,
|
||||
` %f1s = lshr i64 %h, 44`,
|
||||
` %f1a = and i64 %f1s, 63`,
|
||||
` %f1 = shl i64 1, %f1a`,
|
||||
` %f2s = lshr i64 %h, 50`,
|
||||
` %f2a = and i64 %f2s, 63`,
|
||||
` %f2 = shl i64 1, %f2a`,
|
||||
` %fb = or i64 %f1, %f2`,
|
||||
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
|
||||
` %filter.bits = load i64, ptr %fp`,
|
||||
` %fm = and i64 %filter.bits, %fb`,
|
||||
` %fin = icmp eq i64 %fm, %fb`,
|
||||
` br i1 %fin, label %scan, label %miss`,
|
||||
// A number-key entry has no cached hash; a dead entry's key matches no key.
|
||||
`scan:`,
|
||||
` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
|
||||
...(sz === "i64"
|
||||
? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`]
|
||||
: [` %ne = load i32, ptr %nep`]),
|
||||
` br label %scan.loop`,
|
||||
`scan.loop:`,
|
||||
` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`,
|
||||
` %sdone = icmp eq ${sz} %si, %ne`,
|
||||
` br i1 %sdone, label %miss, label %scan.entry`,
|
||||
`scan.entry:`,
|
||||
` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`,
|
||||
` %sk = load i64, ptr %se`,
|
||||
` %skq = icmp eq i64 %sk, %kb`,
|
||||
` br i1 %skq, label %scan.found, label %scan.next`,
|
||||
`scan.found:`,
|
||||
` ret ptr %se`,
|
||||
`scan.next:`,
|
||||
` %si.next = add ${sz} %si, 1`,
|
||||
` br label %scan.loop`,
|
||||
`buckets:`,
|
||||
` %wide = icmp ugt ${sz} %nb, 16777216`,
|
||||
` br i1 %wide, label %runtime, label %probe`,
|
||||
`probe:`,
|
||||
` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`,
|
||||
` %bk = load ptr, ptr %bp`,
|
||||
` %mask = sub ${sz} %nb, 1`,
|
||||
` %ht = lshr i64 %h, 32`,
|
||||
` %ht32 = trunc i64 %ht to i32`,
|
||||
` %tag = and i32 %ht32, -16777216`,
|
||||
sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`,
|
||||
` %i0 = and ${sz} %hi, %mask`,
|
||||
` br label %probe.loop`,
|
||||
`probe.loop:`,
|
||||
` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`,
|
||||
` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`,
|
||||
` %b = load i32, ptr %bep`,
|
||||
` %bempty = icmp eq i32 %b, 0`,
|
||||
` br i1 %bempty, label %miss, label %probe.tag`,
|
||||
`probe.tag:`,
|
||||
` %bt = and i32 %b, -16777216`,
|
||||
` %btq = icmp eq i32 %bt, %tag`,
|
||||
` br i1 %btq, label %probe.entry, label %probe.next`,
|
||||
`probe.entry:`,
|
||||
` %bix = and i32 %b, 16777215`,
|
||||
` %bix1 = sub i32 %bix, 1`,
|
||||
sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`,
|
||||
` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`,
|
||||
` %pk = load i64, ptr %pe`,
|
||||
` %pkq = icmp eq i64 %pk, %kb`,
|
||||
` br i1 %pkq, label %probe.found, label %probe.next`,
|
||||
`probe.found:`,
|
||||
` ret ptr %pe`,
|
||||
`probe.next:`,
|
||||
` %pi.inc = add ${sz} %pi, 1`,
|
||||
` %pi.next = and ${sz} %pi.inc, %mask`,
|
||||
` br label %probe.loop`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`runtime:`,
|
||||
` %r = call ptr @scr_map_entry_f64(ptr %m, double ${runtimeKey}) memory(read) nounwind willreturn`,
|
||||
` ret ptr %r`,
|
||||
`}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
/** The shared abort helpers (emitted only when referenced): the OOM
|
||||
* abort of untraced shape allocation and the invalid-union-tag abort —
|
||||
* both print the runtime ABI’s exact message on fd 2 and abort. */
|
||||
@@ -1333,118 +1537,34 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` %dense.any = icmp ne i64 %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word, its
|
||||
// scr_key_mix hash (zero remapped to one) and the absent-key filter.
|
||||
`define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %nan = fcmp uno double %k, %k`,
|
||||
` %zero = fcmp oeq double %k, 0.0`,
|
||||
` %kz = select i1 %zero, double 0.0, double %k`,
|
||||
` %kzb = bitcast double %kz to i64`,
|
||||
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
|
||||
` %x1s = lshr i64 %kb, 30`,
|
||||
` %x1 = xor i64 %kb, %x1s`,
|
||||
` %x2 = mul i64 %x1, -4658895280553007687`,
|
||||
` %x3s = lshr i64 %x2, 27`,
|
||||
` %x3 = xor i64 %x2, %x3s`,
|
||||
` %x4 = mul i64 %x3, -7723592293110705685`,
|
||||
` %x5s = lshr i64 %x4, 31`,
|
||||
` %x5 = xor i64 %x4, %x5s`,
|
||||
` %hz = icmp eq i64 %x5, 0`,
|
||||
` %h = select i1 %hz, i64 1, i64 %x5`,
|
||||
` %f1s = lshr i64 %h, 44`,
|
||||
` %f1a = and i64 %f1s, 63`,
|
||||
` %f1 = shl i64 1, %f1a`,
|
||||
` %f2s = lshr i64 %h, 50`,
|
||||
` %f2a = and i64 %f2s, 63`,
|
||||
` %f2 = shl i64 1, %f2a`,
|
||||
` %fb = or i64 %f1, %f2`,
|
||||
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
|
||||
` %filter = load i64, ptr %fp`,
|
||||
` %fm = and i64 %filter, %fb`,
|
||||
` %fin = icmp eq i64 %fm, %fb`,
|
||||
` br i1 %fin, label %table, label %miss`,
|
||||
`table:`,
|
||||
` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`,
|
||||
...(sz === "i64"
|
||||
? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`]
|
||||
: [` %nb = load i32, ptr %nbp`]),
|
||||
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %ents = load ptr, ptr %eps`,
|
||||
` %linear = icmp eq ${sz} %nb, 0`,
|
||||
` br i1 %linear, label %scan, label %buckets`,
|
||||
// Small maps keep no bucket table: scan the entries' keys. A number-key
|
||||
// entry has no cached hash; a dead entry's key matches no key.
|
||||
`scan:`,
|
||||
` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
|
||||
...(sz === "i64"
|
||||
? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`]
|
||||
: [` %ne = load i32, ptr %nep`]),
|
||||
` br label %scan.loop`,
|
||||
`scan.loop:`,
|
||||
` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`,
|
||||
` %sdone = icmp eq ${sz} %si, %ne`,
|
||||
` br i1 %sdone, label %miss, label %scan.entry`,
|
||||
`scan.entry:`,
|
||||
` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`,
|
||||
` %sk = load i64, ptr %se`,
|
||||
` %skq = icmp eq i64 %sk, %kb`,
|
||||
` br i1 %skq, label %scan.found, label %scan.next`,
|
||||
`scan.found:`,
|
||||
` ret ptr %se`,
|
||||
`scan.next:`,
|
||||
` %si.next = add ${sz} %si, 1`,
|
||||
` br label %scan.loop`,
|
||||
// Compact tables tag each 24-bit entry index (+1) with the hash's
|
||||
// top byte; wider tables take the runtime lookup.
|
||||
`buckets:`,
|
||||
` %wide = icmp ugt ${sz} %nb, 16777216`,
|
||||
` br i1 %wide, label %runtime, label %probe`,
|
||||
`probe:`,
|
||||
` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`,
|
||||
` %bk = load ptr, ptr %bp`,
|
||||
` %mask = sub ${sz} %nb, 1`,
|
||||
` %ht = lshr i64 %h, 32`,
|
||||
` %ht32 = trunc i64 %ht to i32`,
|
||||
` %tag = and i32 %ht32, -16777216`,
|
||||
sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`,
|
||||
` %i0 = and ${sz} %hi, %mask`,
|
||||
` br label %probe.loop`,
|
||||
`probe.loop:`,
|
||||
` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`,
|
||||
` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`,
|
||||
` %b = load i32, ptr %bep`,
|
||||
` %bempty = icmp eq i32 %b, 0`,
|
||||
` br i1 %bempty, label %miss, label %probe.tag`,
|
||||
`probe.tag:`,
|
||||
` %bt = and i32 %b, -16777216`,
|
||||
` %btq = icmp eq i32 %bt, %tag`,
|
||||
` br i1 %btq, label %probe.entry, label %probe.next`,
|
||||
`probe.entry:`,
|
||||
` %bix = and i32 %b, 16777215`,
|
||||
` %bix1 = sub i32 %bix, 1`,
|
||||
sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`,
|
||||
` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`,
|
||||
` %pk = load i64, ptr %pe`,
|
||||
` %pkq = icmp eq i64 %pk, %kb`,
|
||||
` br i1 %pkq, label %probe.found, label %probe.next`,
|
||||
`probe.found:`,
|
||||
` ret ptr %pe`,
|
||||
`probe.next:`,
|
||||
` %pi.inc = add ${sz} %pi, 1`,
|
||||
` %pi.next = and ${sz} %pi.inc, %mask`,
|
||||
` br label %probe.loop`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`runtime:`,
|
||||
` %r = call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn`,
|
||||
` ret ptr %r`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word and its
|
||||
// scr_key_mix hash. sc_map_entry_f64_int's probe converts the integer
|
||||
// view to the key word, which is never NaN or -0, and so skips the
|
||||
// normalization.
|
||||
...hashedNumberProbe(
|
||||
"sc_map_entry_f64_hashed",
|
||||
"double %k",
|
||||
sz,
|
||||
[
|
||||
` %nan = fcmp uno double %k, %k`,
|
||||
` %zero = fcmp oeq double %k, 0.0`,
|
||||
` %kz = select i1 %zero, double 0.0, double %k`,
|
||||
` %kzb = bitcast double %kz to i64`,
|
||||
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
|
||||
],
|
||||
"%k",
|
||||
),
|
||||
...hashedNumberProbe(
|
||||
"sc_map_entry_f64_hashed_int",
|
||||
"i64 %ki",
|
||||
sz,
|
||||
[` %kf = sitofp i64 %ki to double`, ` %kb = bitcast double %kf to i64`],
|
||||
"%kf",
|
||||
),
|
||||
);
|
||||
}
|
||||
// Map/Set iteration steps read the entries array inline: an index is
|
||||
@@ -1677,8 +1797,8 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// order; other differing words decide by their first differing byte,
|
||||
// and strings shorter than a word by their first byte. Otherwise a
|
||||
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
|
||||
// other pairs call scr_str_cmp_u16_bytes, which only reads the two
|
||||
// strings, so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// other pairs call scr_str_cmp_u16_bytes. Both only read the two strings,
|
||||
// so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// only test the sign of the result.
|
||||
if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
|
||||
const fn = "scr_str_cmp_u16";
|
||||
@@ -1808,7 +1928,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` %anyascii = icmp slt i32 %cws, 0`,
|
||||
` br i1 %anyascii, label %memorder, label %units`,
|
||||
`memorder:`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
` ret i32 %m`,
|
||||
`units:`,
|
||||
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* LlEmitter's statement emission: the statement switch, try/catch, switch, loop copies, speculative writes and string self-concatenation. */
|
||||
import { emitLoopBytesHeaders } from "./bytes-headers.js";
|
||||
import { noteBytesReceiver } from "./expr-bytes.js";
|
||||
import {
|
||||
type LlEmitter,
|
||||
type LlScopeEntry,
|
||||
@@ -50,6 +52,7 @@ import {
|
||||
matchIntegerBytesForLoop,
|
||||
matchIntegerCountedForLoop,
|
||||
matchIntegerPairedForLoop,
|
||||
steppedLoopCounter,
|
||||
} from "../../ir/integer-loops.js";
|
||||
import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js";
|
||||
import {
|
||||
@@ -80,6 +83,7 @@ import {
|
||||
planProvenIntegerLoop,
|
||||
planSpeculativeLoop,
|
||||
type ProvenLoopPlan,
|
||||
type SpeculativeLoopPlan,
|
||||
speculativeReads,
|
||||
speculativeWriteTarget,
|
||||
} from "./speculative-integers.js";
|
||||
@@ -91,6 +95,8 @@ import {
|
||||
} from "./string-construction.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { LOOPS } from "./loop-state.js";
|
||||
import { loopAccessesTypedArrays } from "../loop-polls.js";
|
||||
import { emitConstantChoice, matchConstantChoice } from "./constant-choices.js";
|
||||
|
||||
/** A callback loop version copies the loop; keep the copies small. */
|
||||
const MAX_CALLBACK_VERSION_COST = 400;
|
||||
@@ -478,6 +484,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitter.publishedTypes?.containerGuarded(s.arr.type) !== true
|
||||
)
|
||||
break;
|
||||
// The literal it follows was constructed with this capacity
|
||||
// (reservedArrayLength).
|
||||
if (emitter.consumedReserves.delete(s)) break;
|
||||
const arr = emitBorrowedInput(emitter, s.arr);
|
||||
const length = emitter.emitExpr(s.length);
|
||||
if (s.arr.type.kind !== "array")
|
||||
@@ -571,6 +580,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
// IrBytesElem is static, so never rediscover it through the
|
||||
// generic runtime switch in a hot loop.
|
||||
const arr = emitter.emitStableReceiver(s.arr, [s.index, s.value]);
|
||||
noteBytesReceiver(emitter, s.arr, arr.name);
|
||||
const idx = emitter.emitExpr(s.index);
|
||||
const v = emitter.emitExpr(s.value);
|
||||
if (s.arr.type.kind !== "bytes")
|
||||
@@ -832,6 +842,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
case "if": {
|
||||
// A chain storing one of several literals to one element stores a
|
||||
// table entry instead (debug builds keep the arms to step through).
|
||||
const choice = emitter.debug === null ? matchConstantChoice(s) : null;
|
||||
if (choice !== null) {
|
||||
emitConstantChoice(emitter, choice);
|
||||
break;
|
||||
}
|
||||
const cond = emitter.emitCondition(s.cond);
|
||||
const trueLabel = B.newLabel("if.t");
|
||||
const joinLabel = B.newLabel("if.j");
|
||||
@@ -1208,9 +1225,92 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.line(`store ptr ${fresh}, ptr ${slot}`);
|
||||
}
|
||||
};
|
||||
const workerBudget = emitter.workerLoopBudget(s);
|
||||
// A counted loop over typed arrays, or a loop to a typed array's
|
||||
// length, polls between blocks of WORKER_POLL_BLOCK iterations instead
|
||||
// of counting a budget in its body: the inner loop over a block then
|
||||
// has no exit besides its own bound, which LLVM can vectorize. A typed
|
||||
// array's length is fixed, so each block can take it as its limit when
|
||||
// the loop does not assign the array's binding.
|
||||
//
|
||||
// Inside another loop of this function, which polls at least once
|
||||
// every WORKER_POLL_BLOCK of its own iterations, a loop needs less:
|
||||
// - a loop whose counters' ranges bound its runs, and those of the
|
||||
// loops in its body, to WORKER_POLL_BLOCK^2 needs no poll;
|
||||
// - an innermost counted loop is strip-mined, typed arrays or not,
|
||||
// and does not poll after its last block. A run of at most
|
||||
// WORKER_POLL_BLOCK iterations then never polls, and the countdown
|
||||
// leaves its body. A run of at most WORKER_UNPOLLED_RUN iterations
|
||||
// is one block, polled once as it leaves.
|
||||
const enclosed =
|
||||
emitter.mod.workers === true &&
|
||||
emitter.jumpTargets.some((target) => target.kind === "loop");
|
||||
const bounded =
|
||||
enclosed && boundedLoopRuns(emitter, s) <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK;
|
||||
const innermost = enclosed && !containsLoop(s.body);
|
||||
const stripMined =
|
||||
!bounded &&
|
||||
capturedLets.length === 0 &&
|
||||
integerSlot !== null &&
|
||||
((countedLoop !== null &&
|
||||
countedLimit !== null &&
|
||||
Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) ||
|
||||
(integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) &&
|
||||
emitter.workerLoopPolls(s) &&
|
||||
(loopAccessesTypedArrays(s) || (countedLoop !== null && innermost));
|
||||
const workerBudget = stripMined || bounded ? null : emitter.workerLoopBudget(s);
|
||||
// One slot for every copy of the loop: a speculative copy resumes an
|
||||
// iteration in another copy's body, which then reads this bound.
|
||||
let blockBound: string | null = null;
|
||||
let runSlot: string | null = null;
|
||||
if (stripMined) {
|
||||
blockBound = B.slot();
|
||||
B.entryAllocas.push(
|
||||
`${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`,
|
||||
);
|
||||
if (innermost) {
|
||||
runSlot = B.slot();
|
||||
B.entryAllocas.push(`${runSlot} = alloca i1 ; cancellation poll after the run`);
|
||||
}
|
||||
}
|
||||
freshenBindings();
|
||||
emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => {
|
||||
if (stripMined) {
|
||||
const counter: StripMinedCounter = countedLoop
|
||||
? {
|
||||
localId: countedLoop.localId,
|
||||
slot: integerSlot!,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
step: countedLoop.step,
|
||||
followers: countedLoop.followers.map((f, i) => ({
|
||||
slot: followerSlots[i]!,
|
||||
step: f.step,
|
||||
})),
|
||||
limit: () => countedLimit!,
|
||||
}
|
||||
: {
|
||||
localId: integerLoop!.localId,
|
||||
slot: integerSlot!,
|
||||
type: emitter.sizeType,
|
||||
signed: false,
|
||||
step: 1,
|
||||
followers: [],
|
||||
limit: () => {
|
||||
const receiver = emitter.emitStableReceiver(integerLoop!.limitReceiver, []);
|
||||
const lenPtr = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(
|
||||
`${lenPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`,
|
||||
);
|
||||
B.line(
|
||||
`${len} = load ${emitter.sizeType}, ptr ${lenPtr}${bytesHeaderAlias(emitter)}`,
|
||||
);
|
||||
return len;
|
||||
},
|
||||
};
|
||||
emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit, runSlot);
|
||||
return;
|
||||
}
|
||||
const lc = B.newLabel("loop.c");
|
||||
const lb = B.newLabel("loop.b");
|
||||
const lu =
|
||||
@@ -2136,7 +2236,9 @@ function declareAliasScopes(emitter: LlEmitter, scopes: readonly string[]): void
|
||||
* fast copy is only emitted when every resume point exists. */
|
||||
/** A typed-array loop version: the guard, then the checked copy, which
|
||||
* runs whenever the guard fails, and the copy whose accesses the guard
|
||||
* proves in bounds (or thread-owned). */
|
||||
* proves in bounds (or thread-owned). In worker programs a version that
|
||||
* proves both also has a copy for views with shared storage, in bounds but
|
||||
* not owned. */
|
||||
function emitVersionedLoop(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "for" | "while" },
|
||||
@@ -2148,7 +2250,11 @@ function emitVersionedLoop(
|
||||
done = B.newLabel("bytes.version.done");
|
||||
const unitsFast =
|
||||
version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined;
|
||||
const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast);
|
||||
const sharedFast =
|
||||
version.sharedCopy && unitsFast === undefined
|
||||
? B.newLabel("bytes.version.fast.shared")
|
||||
: undefined;
|
||||
const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast, sharedFast);
|
||||
const units = guard?.units ?? null;
|
||||
const originalBounds = emitter.bytesBounds,
|
||||
originalOwned = emitter.bytesOwned,
|
||||
@@ -2187,6 +2293,16 @@ function emitVersionedLoop(
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
if (sharedFast !== undefined) {
|
||||
// The bounds hold and a view has shared storage: the accesses skip
|
||||
// their bounds checks and stay shared-safe, without alias scopes
|
||||
// (which worker programs give only thread-owned views).
|
||||
B.startBlock(sharedFast);
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.bytesAliasScopes = originalScopes;
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
}
|
||||
if (unitsFast !== undefined && units !== null) {
|
||||
// The string reads take the receiver's UTF-16 units.
|
||||
B.startBlock(unitsFast);
|
||||
@@ -2269,6 +2385,22 @@ export function emitLoopCopies(
|
||||
else ordinary(exit);
|
||||
return;
|
||||
}
|
||||
const outer = emitter.bytesHeaders;
|
||||
emitter.bytesHeaders = emitLoopBytesHeaders(emitter, loop, emitter.currentParameters) ?? outer;
|
||||
try {
|
||||
emitSpeculativeLoop(emitter, plan, exit, emitCopy);
|
||||
} finally {
|
||||
emitter.bytesHeaders = outer;
|
||||
}
|
||||
}
|
||||
|
||||
/** The copies of a speculative loop (emitLoopCopies). */
|
||||
function emitSpeculativeLoop(
|
||||
emitter: LlEmitter,
|
||||
plan: SpeculativeLoopPlan,
|
||||
exit: string,
|
||||
emitCopy: (exit: string) => void,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
const entries = new Map<string, string>();
|
||||
let ok = "true";
|
||||
@@ -2546,6 +2678,230 @@ function emitInt32KindLoop(
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
/** Iterations between cancellation polls of a strip-mined counted loop. */
|
||||
const WORKER_POLL_BLOCK = 64;
|
||||
|
||||
/** The longest run of a strip-mined loop inside another loop that polls
|
||||
* only as it leaves. */
|
||||
const WORKER_UNPOLLED_RUN = 16384;
|
||||
|
||||
/** Whether a step of `step` from every value in `range` gives a different
|
||||
* value. Beyond 2^53 a double can stop changing (`j + 1 === j` from 2^53),
|
||||
* so a counter there can run without end inside a range of a few values. */
|
||||
function exactSteps(range: { min: number; max: number }, step: number): boolean {
|
||||
const size = Math.abs(step);
|
||||
return range.min >= size - 2 ** 53 && range.max <= 2 ** 53 - size;
|
||||
}
|
||||
|
||||
/** At most how many times a `for` loop and the loops in its body run their
|
||||
* bodies per entry, from the counters' proven ranges: its iterations times
|
||||
* one plus the nested loops' runs. Infinity when a count has no bound, or
|
||||
* depends on a run-time guard (the guard's slow copy steps a double). */
|
||||
function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number {
|
||||
if (loop.kind !== "for") return Infinity;
|
||||
const counted = matchIntegerCountedForLoop(
|
||||
loop,
|
||||
emitter.numericLocals,
|
||||
emitter.integerRanges,
|
||||
LOOPS.of(emitter).unaliasedArrays,
|
||||
);
|
||||
let trips = Infinity;
|
||||
if (counted && !counted.guarded && exactSteps(counted.range, counted.step))
|
||||
trips = Math.floor((counted.range.max - counted.range.min) / Math.abs(counted.step)) + 1;
|
||||
else if (!counted) {
|
||||
// Each iteration closes the gap between the two counters by at least two.
|
||||
const paired = matchIntegerPairedForLoop(loop, emitter.numericLocals, emitter.integerRanges);
|
||||
if (paired && !paired.guarded && exactSteps(paired.range, 1))
|
||||
trips = Math.floor((paired.range.max - paired.range.min) / 2) + 1;
|
||||
}
|
||||
if (trips === Infinity) {
|
||||
// A counter with a proven range where the condition reads it, such as
|
||||
// one compared with a module constant.
|
||||
const stepped = steppedLoopCounter(loop, emitter.currentLocals);
|
||||
const range = stepped && emitter.integerRanges.get(stepped.ref);
|
||||
if (stepped && range && exactSteps(range, stepped.step))
|
||||
trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1;
|
||||
}
|
||||
if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity;
|
||||
let nested = 0;
|
||||
const visit = (s: IrStmt): boolean => {
|
||||
if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") {
|
||||
nested += boundedLoopRuns(emitter, s);
|
||||
return nested !== Infinity;
|
||||
}
|
||||
return everyStmtChild(s, () => true, visit);
|
||||
};
|
||||
if (!loop.body.every(visit)) return Infinity;
|
||||
return trips * (1 + nested);
|
||||
}
|
||||
|
||||
/** The counter of a strip-mined loop: its binding, its slot, the LLVM
|
||||
* integer type and signedness of its values, its step, the counters stepped
|
||||
* beside it, and the limit, emitted in the outer header before each block. */
|
||||
interface StripMinedCounter {
|
||||
localId: string;
|
||||
slot: string;
|
||||
type: string;
|
||||
signed: boolean;
|
||||
step: number;
|
||||
followers: { slot: string; step: number }[];
|
||||
limit: () => string;
|
||||
}
|
||||
|
||||
/** A counted `for` loop of a worker program as blocks of WORKER_POLL_BLOCK
|
||||
* iterations, with the cancellation poll between blocks: the outer header
|
||||
* tests the counter against the loop's limit, each block runs the body up
|
||||
* to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll
|
||||
* follows the block. It polls as often as the budget it replaces, and the
|
||||
* inner loop's only exit besides `break` and `return` is its bound.
|
||||
*
|
||||
* With `runSlot` (for a loop that another loop of the function encloses), a
|
||||
* block that ends at the limit leaves the loop without polling, and a run
|
||||
* of at most WORKER_UNPOLLED_RUN iterations is one block, polled once as
|
||||
* it leaves when it took more than WORKER_POLL_BLOCK iterations. `runSlot`
|
||||
* holds whether the current block is such a run. */
|
||||
function emitStripMinedCountedLoop(
|
||||
emitter: LlEmitter,
|
||||
s: Extract<IrStmt, { kind: "for" }>,
|
||||
counter: StripMinedCounter,
|
||||
boundSlot: string,
|
||||
exit: string,
|
||||
runSlot: string | null,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
const { slot, type, signed, step } = counter;
|
||||
const ascending = step > 0;
|
||||
const compare = !signed ? "ult" : ascending ? "slt" : "sgt";
|
||||
const saturating = `llvm.${signed ? "sadd" : "uadd"}.sat.${type}`;
|
||||
const nearer = `llvm.${!signed ? "umin" : ascending ? "smin" : "smax"}.${type}`;
|
||||
const wrap = signed ? "nsw" : "nuw";
|
||||
const lc = B.newLabel("loop.c");
|
||||
const block = B.newLabel("loop.block");
|
||||
const bc = B.newLabel("loop.bc");
|
||||
const lb = B.newLabel("loop.b");
|
||||
const lu = B.newLabel("loop.u");
|
||||
const poll = B.newLabel("loop.poll");
|
||||
emitter.declare(`declare ${type} @${saturating}(${type}, ${type})`);
|
||||
emitter.declare(`declare ${type} @${nearer}(${type}, ${type})`);
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
enterLoopHeader(emitter, lc);
|
||||
const start = B.tmp();
|
||||
const open = B.tmp();
|
||||
B.line(`${start} = load ${type}, ptr ${slot}`);
|
||||
const limit = counter.limit();
|
||||
B.line(`${open} = icmp ${compare} ${type} ${start}, ${limit}`);
|
||||
B.condBr(open, block, exit);
|
||||
B.startBlock(block);
|
||||
const stepped = B.tmp();
|
||||
const bound = B.tmp();
|
||||
B.line(
|
||||
`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`,
|
||||
);
|
||||
B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`);
|
||||
if (runSlot === null) B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
|
||||
else {
|
||||
// The steps left to the limit: positive, as the header tested.
|
||||
const left = B.tmp(),
|
||||
whole = B.tmp(),
|
||||
long = B.tmp(),
|
||||
polled = B.tmp(),
|
||||
end = B.tmp();
|
||||
const stride = Math.abs(step);
|
||||
B.line(
|
||||
ascending
|
||||
? `${left} = sub ${wrap} ${type} ${limit}, ${start}`
|
||||
: `${left} = sub ${wrap} ${type} ${start}, ${limit}`,
|
||||
);
|
||||
B.line(`${whole} = icmp ule ${type} ${left}, ${stride * WORKER_UNPOLLED_RUN}`);
|
||||
B.line(`${long} = icmp ugt ${type} ${left}, ${stride * WORKER_POLL_BLOCK}`);
|
||||
B.line(`${polled} = and i1 ${whole}, ${long}`);
|
||||
B.line(`${end} = select i1 ${whole}, ${type} ${limit}, ${type} ${bound}`);
|
||||
B.line(`store ${type} ${end}, ptr ${boundSlot}`);
|
||||
B.line(`store i1 ${polled}, ptr ${runSlot}`);
|
||||
}
|
||||
B.br(bc);
|
||||
B.startBlock(bc);
|
||||
const index = B.tmp();
|
||||
const end = B.tmp();
|
||||
const inBlock = B.tmp();
|
||||
B.line(`${index} = load ${type}, ptr ${slot}`);
|
||||
B.line(`${end} = load ${type}, ptr ${boundSlot}`);
|
||||
B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`);
|
||||
const after = runSlot === null ? poll : B.newLabel("loop.end");
|
||||
B.condBr(inBlock, lb, after);
|
||||
B.startBlock(lb);
|
||||
const target: LlEmitter["jumpTargets"][number] = {
|
||||
kind: "loop",
|
||||
brkLabel: exit,
|
||||
contLabel: lu,
|
||||
labels: s.labels,
|
||||
frameDepth: emitter.frames.length,
|
||||
scopeDepth: emitter.scopes.length,
|
||||
finallyDepth: emitter.finallyStack.length,
|
||||
};
|
||||
emitter.jumpTargets.push(target);
|
||||
// The counter steps by a nonzero constant and the body does not assign
|
||||
// it, so an element at the counter is a different element on every
|
||||
// iteration: its plain load or store is not merged with another
|
||||
// iteration's or moved out of the loop.
|
||||
const outer = emitter.plainSharedLoop;
|
||||
emitter.plainSharedLoop = { counter: counter.localId, target, depth: emitter.loopDepth };
|
||||
try {
|
||||
emitter.emitBlock(s.body);
|
||||
} finally {
|
||||
emitter.plainSharedLoop = outer;
|
||||
}
|
||||
emitter.jumpTargets.pop();
|
||||
B.br(lu);
|
||||
B.startBlock(lu);
|
||||
for (const { slot: stepSlot, step: by } of [{ slot, step }, ...counter.followers]) {
|
||||
const old = B.tmp();
|
||||
const next = B.tmp();
|
||||
B.line(`${old} = load ${type}, ptr ${stepSlot}`);
|
||||
B.line(`${next} = add ${wrap} ${type} ${old}, ${by}`);
|
||||
B.line(`store ${type} ${next}, ptr ${stepSlot}`);
|
||||
}
|
||||
B.br(bc);
|
||||
if (runSlot !== null) {
|
||||
// A speculative copy may enter the body without passing the header, so
|
||||
// the limit is read again here.
|
||||
B.startBlock(after);
|
||||
const at = B.tmp();
|
||||
const more = B.tmp();
|
||||
const run = B.tmp();
|
||||
const done = B.newLabel("loop.done");
|
||||
const last = B.newLabel("loop.poll.run");
|
||||
B.line(`${at} = load ${type}, ptr ${slot}`);
|
||||
B.line(`${more} = icmp ${compare} ${type} ${at}, ${counter.limit()}`);
|
||||
B.condBr(more, poll, done);
|
||||
B.startBlock(done);
|
||||
B.line(`${run} = load i1, ptr ${runSlot}`);
|
||||
B.condBr(run, last, exit);
|
||||
B.startBlock(last);
|
||||
emitter.emitWorkerPoll();
|
||||
B.br(exit);
|
||||
}
|
||||
B.startBlock(poll);
|
||||
emitter.emitWorkerPoll();
|
||||
B.br(lc);
|
||||
}
|
||||
|
||||
/** Whether `body` holds a loop outside its closures. */
|
||||
function containsLoop(body: IrStmt[]): boolean {
|
||||
const inStmt = (s: IrStmt): boolean =>
|
||||
s.kind === "while" ||
|
||||
s.kind === "doWhile" ||
|
||||
s.kind === "for" ||
|
||||
s.kind === "forOf" ||
|
||||
!everyStmtChild(
|
||||
s,
|
||||
() => true,
|
||||
(child) => !inStmt(child),
|
||||
);
|
||||
return body.some(inStmt);
|
||||
}
|
||||
|
||||
function enterLoopHeader(emitter: LlEmitter, header: string): void {
|
||||
const dispatch = emitter.speculationHeader;
|
||||
emitter.speculationHeader = null;
|
||||
|
||||
@@ -211,6 +211,19 @@ test("a target CPU level reaches every function attribute group and nothing else
|
||||
expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain);
|
||||
});
|
||||
|
||||
test("only sanitized builds mark the emitted functions sanitize_address", () => {
|
||||
const module = moduleFor([call()]);
|
||||
const sanitized = emitLlvmModule(module);
|
||||
expect(emitLlvmModule(module, { sanitizeAttributes: true })).toBe(sanitized);
|
||||
expect(sanitized).toMatch(/^attributes #0 = \{ sanitize_address \}$/m);
|
||||
const plain = emitLlvmModule(module, { sanitizeAttributes: false });
|
||||
expect(plain).not.toContain("sanitize_address");
|
||||
// A group needs an attribute: an ignored string attribute stands in.
|
||||
expect(plain).toMatch(/^attributes #0 = \{ "scriptc-function" \}$/m);
|
||||
const v3 = emitLlvmModule(module, { sanitizeAttributes: false, targetCpu: "x86-64-v3" });
|
||||
expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m);
|
||||
});
|
||||
|
||||
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
|
||||
for (const boxed of [false, true]) {
|
||||
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TargetCpu } from "../target-cpu.js";
|
||||
import type { FunctionLayout } from "./function-layout.js";
|
||||
import { relieveHeapPressure } from "../../heap-pressure.js";
|
||||
import { wasmBoundsChecks } from "./wasm-modules.js";
|
||||
import { jsTruthy } from "./jsval.js";
|
||||
@@ -63,6 +64,7 @@ import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
import { matchBytesLoopVersion } from "./bytes-loop-versions.js";
|
||||
import { globalSlotAlias } from "./bytes-alias.js";
|
||||
import type { BytesHeaderScope } from "./bytes-headers.js";
|
||||
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
|
||||
import {
|
||||
materializeSplitPiece,
|
||||
@@ -147,6 +149,7 @@ import {
|
||||
bridgePlainUnwindCalls,
|
||||
unwindAbiFunctions,
|
||||
} from "./unwind-abi.js";
|
||||
import { cloneForCpuDispatch } from "./cpu-dispatch.js";
|
||||
import {
|
||||
mangleAsyncInlineAwaitCall,
|
||||
mangleAsyncSpawn,
|
||||
@@ -183,14 +186,14 @@ import {
|
||||
unionBoxBits,
|
||||
} from "./inline-unions.js";
|
||||
import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js";
|
||||
import { CheckedNarrows } from "./checked-narrows.js";
|
||||
import { borrowsCheckedNarrow, CheckedNarrows } from "./checked-narrows.js";
|
||||
import {
|
||||
emitDenseArraySet,
|
||||
emitDenseSlotUnlink,
|
||||
SCR_ELEM_I32,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { matchMapRead, pointerMapGet, type MapReadLifetimes } from "./map-read-lifetimes.js";
|
||||
import { type MapEntryFusion } from "./map-entry-fusion.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
@@ -202,7 +205,7 @@ import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { ReferenceWrites, type Writes } from "./reference-writes.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { StackCallbacks } from "./stack-callbacks.js";
|
||||
import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { findAppendedArrayLiterals, reservedArrayLength } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findLentRecordFields,
|
||||
@@ -213,7 +216,13 @@ import {
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
import { StackCaptures } from "./stack-captures.js";
|
||||
import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD, SCR_ARR_FROZEN } from "./common.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
emitFieldAbsentTest,
|
||||
f64Lit,
|
||||
SCR_ARR_FIELD,
|
||||
SCR_ARR_FROZEN,
|
||||
} from "./common.js";
|
||||
import { ffiExtendsNarrowIntegers } from "../targets.js";
|
||||
import {
|
||||
emitLiteralExpr,
|
||||
@@ -285,7 +294,12 @@ import {
|
||||
emitBytesSet,
|
||||
emitBytesIntrinsic,
|
||||
} from "./expr-bytes.js";
|
||||
import { emitRegexIntrinsic, emitRecordKeyGet, keyedRecordReadInto } from "./expr-records.js";
|
||||
import {
|
||||
emitRecordKeyGet,
|
||||
emitRegexIntrinsic,
|
||||
keyedRecordReadInto,
|
||||
pointerRecordKeyGet,
|
||||
} from "./expr-records.js";
|
||||
import {
|
||||
dynPromiseAdapter,
|
||||
streamTypedRefCommitAdapter,
|
||||
@@ -476,18 +490,19 @@ export interface LlvmTargetOptions {
|
||||
* earlier emission found breaking its rules (UnwindAbiViolation). */
|
||||
unwindExcluded?: ReadonlySet<string>;
|
||||
/** Emit the function bodies in this many contiguous ranges, each by its
|
||||
* own emitter, and merge them in function order (emit-journal.ts). The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
* emit in one emitter. */
|
||||
* own emitter, and merge them in function order (emit-journal.ts, and
|
||||
* debug-info.ts for debug metadata). The output is the same as one
|
||||
* emitter's. */
|
||||
functionChunks?: number;
|
||||
/** Emit the function bodies of a large module on this many forked worker
|
||||
* processes at a time (emit-module.ts), forked with `forkPrimitives`. The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
* emit in one emitter. */
|
||||
* output is the same as one emitter's. */
|
||||
functionWorkers?: number;
|
||||
/** The host's process primitives, where it can fork (the native
|
||||
* compiler's). */
|
||||
forkPrimitives?: ForkPrimitives | undefined;
|
||||
/** Functions to write first, by tier (function-layout.ts). */
|
||||
functionLayout?: FunctionLayout;
|
||||
/** Exact frontend sources for dev-build locations and variable metadata. */
|
||||
debugSources?: ReadonlyMap<string, string>;
|
||||
/** Pointer width of the target C ABI. Native targets are 64-bit today. */
|
||||
@@ -512,6 +527,12 @@ export interface LlvmTargetOptions {
|
||||
inlineRc?: InlineRcMode;
|
||||
/** Define the inline RC fast-path helpers `noinline` (sanitized builds). */
|
||||
rcHelpersNoinline?: boolean;
|
||||
/** Mark every emitted function `sanitize_address`, which the sanitized
|
||||
* lane's `-fsanitize=address` link needs to instrument it. Without a
|
||||
* sanitizer pass the attribute still turns off shrink-wrapping and
|
||||
* speculative loads, so plain builds pass false. Defaults to true for
|
||||
* direct emission. */
|
||||
sanitizeAttributes?: boolean;
|
||||
/** Define `scr_detached_exit` so the Linux runtime hands the address-space
|
||||
* teardown after `process.exit` to a helper process (`--detached-exit`).
|
||||
* Executables only; the runtime's weak default keeps it off. */
|
||||
@@ -528,6 +549,11 @@ export interface LlvmTargetOptions {
|
||||
* every emitted function carries it as its `target-cpu`. Unset, code
|
||||
* runs on any x86-64 CPU. */
|
||||
targetCpu?: TargetCpu;
|
||||
/** Give an x86-64 executable without a `targetCpu` an x86-64-v3 copy of
|
||||
* each program function, which it runs on CPUs at that level
|
||||
* (cpu-dispatch.ts). Ignored for other targets, libraries and builds
|
||||
* with debug information. */
|
||||
cpuDispatch?: boolean;
|
||||
}
|
||||
|
||||
/** LlvmTargetOptions.lowRegion for an executable: Linux targets without
|
||||
@@ -1270,6 +1296,10 @@ export class LlEmitter {
|
||||
readonly runtimeAbiMarker: boolean;
|
||||
readonly detachedExit: boolean;
|
||||
readonly targetCpu: TargetCpu | undefined;
|
||||
/** Emitted functions carry `sanitize_address` (LlvmTargetOptions). */
|
||||
readonly sanitizeAttributes: boolean;
|
||||
/** LlvmTargetOptions.cpuDispatch, where it applies. */
|
||||
readonly cpuDispatch: boolean;
|
||||
readonly wasmBoundsChecks: boolean;
|
||||
/** Interned string literals: UTF-8 text → { symbol, byte length } —
|
||||
* first-use order, the runtime ABI’s determinism discipline. */
|
||||
@@ -1304,6 +1334,12 @@ export class LlEmitter {
|
||||
string,
|
||||
{ sym: string; src: string; fl: string; pattern: string; flags: string }
|
||||
>();
|
||||
/** Build-time replacers: one per (pattern, flags, replacement) of a
|
||||
* global substitution with a literal replacement (regex-matchers.ts). */
|
||||
readonly regexReplacers = new Map<
|
||||
string,
|
||||
{ sym: string; pattern: string; flags: string; replacement: string }
|
||||
>();
|
||||
/** Interned tagged-template strings objects: per-site key → { symbol,
|
||||
* interned cooked-literal refs }. One immortal ScrArr of string slots
|
||||
* per template SITE (the spec's per-occurrence identity — the C
|
||||
@@ -1479,6 +1515,11 @@ export class LlEmitter {
|
||||
/** Locals whose array literal emitStmts left for the `await Promise.all`
|
||||
* that reads them (deferredPromiseAllLocal). */
|
||||
private readonly deferredPromiseAllEntries = new Map<string, ExprOf<"arrayLit">>();
|
||||
/** Empty array literals that a capacity hint follows directly
|
||||
* (reservedArrayLength), with that hint. Constructing the literal with
|
||||
* the hinted capacity consumes the hint, which then emits nothing. */
|
||||
readonly reservedArrayLengths = new Map<IrExpr, IrStmt & { kind: "arraySetLength" }>();
|
||||
readonly consumedReserves = new Set<IrStmt>();
|
||||
/** The promise a `return` emitted past its finally regions already
|
||||
* produced for its adopting await (borrowed from the return's parked
|
||||
* slot); that await takes it instead of evaluating its operand. */
|
||||
@@ -1629,6 +1670,9 @@ export class LlEmitter {
|
||||
* commit back into their objects. Library builds keep their commits:
|
||||
* their values cross a native boundary. */
|
||||
readonly liveViewsReadOnly: boolean;
|
||||
/** IrModule.foreignElementKeys: element keys whose arrays can hold
|
||||
* SCR_ARR_FOREIGN elements. */
|
||||
readonly foreignElementKeys: ReadonlySet<string>;
|
||||
/** The program freezes, seals or prevents extensions of untyped values
|
||||
* (IrModule.untypedIntegrity): the commits of class views store the
|
||||
* view's integrity state in the instance's integrity word. */
|
||||
@@ -1783,11 +1827,26 @@ export class LlEmitter {
|
||||
/** Worker programs: typed-array accesses whose views an enclosing version
|
||||
* guard proved thread-owned. */
|
||||
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
|
||||
/** Worker programs: the strip-mined counted loop whose body is being
|
||||
* emitted (emitStripMinedCountedLoop), with its counter, its jump target
|
||||
* and its loopDepth. An element access of a view that may be shared, at
|
||||
* the counter and directly in that loop's body, is a plain load or store
|
||||
* (plainSharedElement in expr-bytes.ts). */
|
||||
plainSharedLoop: {
|
||||
counter: string;
|
||||
target: LlEmitter["jumpTargets"][number];
|
||||
depth: number;
|
||||
} | null = null;
|
||||
/** The alias-scope attachment of each typed-array element read and write
|
||||
* in the current loop version's fast copy (emitVersionedLoop), and the
|
||||
* one the element access being emitted carries (bytesElementAlias). */
|
||||
bytesAliasScopes: ReadonlyMap<IrExpr | IrStmt, string> = new Map();
|
||||
bytesAccessScope = "";
|
||||
/** Header fields a speculative loop's entry read for its views
|
||||
* (bytes-headers.ts), while its copies are emitted. */
|
||||
bytesHeaders: BytesHeaderScope | null = null;
|
||||
/** The parameters of the function being emitted. */
|
||||
currentParameters: ReadonlySet<string> = new Set();
|
||||
/** Loop versions with alias scopes in the current function; it names
|
||||
* their scopes. */
|
||||
aliasScopedVersions = 0;
|
||||
@@ -1884,9 +1943,7 @@ export class LlEmitter {
|
||||
this.journal = journal;
|
||||
this.journaled =
|
||||
journal !== null ||
|
||||
((options.functionWorkers ?? 1) > 1 &&
|
||||
options.forkPrimitives !== undefined &&
|
||||
options.debugSources === undefined);
|
||||
((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined);
|
||||
// One walk of every body, which the analyses below read instead of
|
||||
// walking the bodies themselves.
|
||||
this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod);
|
||||
@@ -1923,6 +1980,7 @@ export class LlEmitter {
|
||||
!this.untypedIntegrity &&
|
||||
!dynamicStores.all &&
|
||||
dynamicStores.names.size === 0;
|
||||
this.foreignElementKeys = new Set(mod.foreignElementKeys ?? []);
|
||||
this.int32Abis = this.debug === null ? findInt32Abis(mod, this.int32Slots) : new Map();
|
||||
this.immortalGlobals = new Set(mod.contracts?.immortalGlobals ?? []);
|
||||
this.programLifetime = new Set(mod.contracts?.programLifetime ?? []);
|
||||
@@ -1959,6 +2017,14 @@ export class LlEmitter {
|
||||
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
|
||||
this.detachedExit = options.detachedExit === true;
|
||||
this.targetCpu = options.targetCpu;
|
||||
this.sanitizeAttributes = options.sanitizeAttributes !== false;
|
||||
this.cpuDispatch =
|
||||
options.cpuDispatch === true &&
|
||||
options.targetCpu === undefined &&
|
||||
!this.wasi &&
|
||||
mod.lib === undefined &&
|
||||
this.debug === null &&
|
||||
/^(x86_64|amd64|x64)\b/.test(options.targetTriple || process.arch);
|
||||
this.wasmBoundsChecks =
|
||||
options.wasmBoundsChecks === true ||
|
||||
wasmBoundsChecks({
|
||||
@@ -2217,6 +2283,7 @@ export class LlEmitter {
|
||||
declare: (decl) => this.declare(decl),
|
||||
needOom: () => this.needOom(),
|
||||
needBadTag: () => this.needBadTag(),
|
||||
mayHoldForeign: (elem) => this.mayHoldForeign(elem),
|
||||
internLiteral: (text) => this.internLiteral(text),
|
||||
cstr: (text) => this.cstr(text),
|
||||
mapOps: (operands) => this.mapOps(operands),
|
||||
@@ -2464,7 +2531,8 @@ export class LlEmitter {
|
||||
}
|
||||
emitParts(): string[] {
|
||||
const parts = emitParts(this);
|
||||
return this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
|
||||
const bridged = this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
|
||||
return this.cpuDispatch ? cloneForCpuDispatch(bridged) : bridged;
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
@@ -2538,7 +2606,7 @@ export class LlEmitter {
|
||||
ret === "void" ? ` ${call}` : ` %r = ${call}`,
|
||||
ret === "void" ? ` ret void` : ` ret ${ret} %r`,
|
||||
`}`,
|
||||
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${(fn.generator ? 1 : 0) + (fn.async ? 2 : 0) + (fn.ownsPrototype ? 4 : 0)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
|
||||
`@${mangleFnClosure(name)} = internal ${this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""}global %ScrClosure { ${this.sizeType} -1, ptr @${mangleWrapper(name)}, ${this.sizeType} 0, ptr null, i32 ${this.closureFunctionKind(fn)}, i32 0, ptr null, ptr ${lending ? `@${mangleBorrowingWrapper(name)}` : "null"} }`,
|
||||
``,
|
||||
);
|
||||
if (lending)
|
||||
@@ -3090,12 +3158,13 @@ export class LlEmitter {
|
||||
|
||||
// ── plumbing (frame and scope ownership) ──────
|
||||
|
||||
/** A symbol derived from the constant's text rather than its first-use
|
||||
* index, so an edit that interns one more string leaves every other
|
||||
* constant's symbol, and the code referencing it, unchanged. This lets
|
||||
* native builds reuse the compiled partitions an edit does not touch. A
|
||||
* collision retries with a counter, still in first-use order. */
|
||||
private contentSymbol(prefix: string, text: string): string {
|
||||
/** A symbol derived from a constant's text, or a helper's key, rather
|
||||
* than its first-use index, so an edit that interns one more string or
|
||||
* helper leaves every other symbol, and the code referencing it,
|
||||
* unchanged. This lets native builds reuse the compiled partitions an
|
||||
* edit does not touch. A collision retries with a counter, still in
|
||||
* first-use order. */
|
||||
contentSymbol(prefix: string, text: string): string {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const digest = createHash("sha256")
|
||||
.update(attempt === 0 ? text : `${attempt}\0${text}`)
|
||||
@@ -3221,7 +3290,7 @@ export class LlEmitter {
|
||||
const key = `${unionId}:${tag}`;
|
||||
let sym = this.unitInstances.get(key);
|
||||
if (!sym) {
|
||||
sym = `sc_unit_${this.unitInstances.size}`;
|
||||
sym = this.contentSymbol("sc_unit_", key);
|
||||
this.unitInstances.set(key, sym);
|
||||
}
|
||||
this.journal?.entry("unit", key, sym);
|
||||
@@ -3678,6 +3747,9 @@ export class LlEmitter {
|
||||
passThrough?: { name: string; type: IrType },
|
||||
poll = false,
|
||||
guard?: string,
|
||||
/** Runs at the start of the cold unwind block, before the unwind (a
|
||||
* package-declared check's origin note). */
|
||||
onUnwind?: () => void,
|
||||
): void {
|
||||
const through =
|
||||
passThrough !== undefined &&
|
||||
@@ -3702,6 +3774,7 @@ export class LlEmitter {
|
||||
const lk = B.newLabel("exc.k");
|
||||
B.condBr(p, lu, lk);
|
||||
B.startBlock(lu);
|
||||
onUnwind?.();
|
||||
this.emitUnwind(through);
|
||||
B.startBlock(lk);
|
||||
if (skip !== undefined) {
|
||||
@@ -3778,6 +3851,7 @@ export class LlEmitter {
|
||||
B.condBr(cold, lu, lk);
|
||||
}
|
||||
B.startBlock(lu);
|
||||
onUnwind?.();
|
||||
this.emitUnwind(through);
|
||||
B.startBlock(lk);
|
||||
if (skip !== undefined) {
|
||||
@@ -3927,13 +4001,19 @@ export class LlEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `loop` needs a cancellation poll: in a worker program, unless
|
||||
* the loop already runs a pending test on every iteration, which observes
|
||||
* a terminate request itself (a separate poll would only add work). */
|
||||
workerLoopPolls(loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" | "forOf" }>): boolean {
|
||||
return (
|
||||
this.mod.workers === true && !loopTestsEveryIteration(loop, (e) => this.callTestsPending(e))
|
||||
);
|
||||
}
|
||||
|
||||
workerLoopBudget(
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" | "forOf" }>,
|
||||
): string | null {
|
||||
if (!this.mod.workers) return null;
|
||||
// A loop that runs a pending test on every iteration observes a
|
||||
// terminate request through it; a separate poll would only add work.
|
||||
if (loopTestsEveryIteration(loop, (e) => this.callTestsPending(e))) return null;
|
||||
if (!this.workerLoopPolls(loop)) return null;
|
||||
const slot = this.B.slot();
|
||||
this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`);
|
||||
this.B.line(`store i32 0, ptr ${slot}`);
|
||||
@@ -4051,14 +4131,19 @@ export class LlEmitter {
|
||||
B.condBr(due, poll, next);
|
||||
B.startBlock(poll);
|
||||
B.line(`store i32 63, ptr ${slot}`);
|
||||
// Calls and throwing operations retain their own checks. Amortize the
|
||||
// extra cancellation poll across bounded loop batches so scalar work
|
||||
// does not cross the runtime ABI on every iteration.
|
||||
this.emitPendingCheck(undefined, true);
|
||||
this.emitWorkerPoll();
|
||||
B.br(next);
|
||||
B.startBlock(next);
|
||||
}
|
||||
|
||||
/** The cancellation poll itself. Calls and throwing operations retain
|
||||
* their own checks. Loops amortize this extra poll across bounded
|
||||
* batches of iterations so scalar work does not cross the runtime ABI on
|
||||
* every iteration. */
|
||||
emitWorkerPoll(): void {
|
||||
this.emitPendingCheck(undefined, true);
|
||||
}
|
||||
|
||||
/** Moves an already-evaluated value into the runtime's exception cell —
|
||||
* the `throw` statement's kind dispatch (stmts.ts's), shared with
|
||||
* every synthetic thrower. Ownership of a refcounted payload must have
|
||||
@@ -5259,6 +5344,7 @@ export class LlEmitter {
|
||||
chunkResult(): ChunkResult {
|
||||
return {
|
||||
entries: this.journal!.entries,
|
||||
debugRequests: this.debug?.recorded() ?? [],
|
||||
flags: {
|
||||
usesCoroutines: this.usesCoroutines,
|
||||
usesTimers: this.usesTimers,
|
||||
@@ -5294,6 +5380,8 @@ export class LlEmitter {
|
||||
// emission left them (a helper set guards the declarations its first
|
||||
// request adds).
|
||||
replayJournal(this, chunk.entries, names, tags);
|
||||
const debugIds = new Map<string, string>();
|
||||
this.debug?.replay(chunk.debugRequests, (name) => this.functionNamed(name), debugIds);
|
||||
const flags = chunk.flags;
|
||||
this.usesCoroutines ||= flags.usesCoroutines;
|
||||
this.usesTimers ||= flags.usesTimers;
|
||||
@@ -5314,7 +5402,13 @@ export class LlEmitter {
|
||||
for (const name of chunk.adoptCallees) this.adoptCallees.add(name);
|
||||
for (const name of chunk.directAwaitCallees) this.directAwaitCallees.add(name);
|
||||
for (const name of chunk.inlineAwaitCallees) this.inlineAwaitCallees.add(name);
|
||||
return renameChunk(names, tags);
|
||||
return renameChunk(names, tags, debugIds);
|
||||
}
|
||||
|
||||
private functionNamed(name: string): IrFunction {
|
||||
const fn = this.fnByName.get(name);
|
||||
if (fn === undefined) throw new InternalCompilerError(`llvm emitter bug: no function ${name}`);
|
||||
return fn;
|
||||
}
|
||||
|
||||
/** Adds a content-named constant a chunk interned. Its symbol is this
|
||||
@@ -5407,8 +5501,15 @@ export class LlEmitter {
|
||||
case "retainWrapper":
|
||||
addEntry(this.nullableUnions.retainWrappers, key, value as string);
|
||||
return;
|
||||
case "unit":
|
||||
return addNumbered(this.unitInstances, key, value as string, "sc_unit_", names);
|
||||
case "unit": {
|
||||
let own = this.unitInstances.get(key);
|
||||
if (own === undefined) {
|
||||
own = this.contentSymbol("sc_unit_", key);
|
||||
this.unitInstances.set(key, own);
|
||||
}
|
||||
if (own !== value) names.set(value as string, own);
|
||||
return;
|
||||
}
|
||||
case "absent":
|
||||
return addNumbered(this.absentInstances, key, value as string, "sc_absent_", names);
|
||||
case "retype":
|
||||
@@ -5416,7 +5517,9 @@ export class LlEmitter {
|
||||
case "slots":
|
||||
return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names);
|
||||
case "regex":
|
||||
return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names);
|
||||
return this.addContentEntry(this.regexInstances, key, value as never, "sc_re_", names);
|
||||
case "regexReplacer":
|
||||
return this.addContentEntry(this.regexReplacers, key, value as never, "sc_rr_", names);
|
||||
case "templateStrings":
|
||||
return addNumberedSymbol(
|
||||
this.templateStringsInstances,
|
||||
@@ -5442,7 +5545,13 @@ export class LlEmitter {
|
||||
/** An emitter for a range of this module's functions: the same facts,
|
||||
* empty module-level tables, and a journal of what the range adds. */
|
||||
chunkEmitter(): LlEmitter {
|
||||
return new LlEmitter(this.mod, { ...this.targetOptions, functionChunks: 1 }, new EmitJournal());
|
||||
const chunk = new LlEmitter(
|
||||
this.mod,
|
||||
{ ...this.targetOptions, functionChunks: 1 },
|
||||
new EmitJournal(),
|
||||
);
|
||||
chunk.debug?.record();
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/** The double-ABI entry of a function with an i32 entry: every caller
|
||||
@@ -5687,7 +5796,10 @@ export class LlEmitter {
|
||||
unwrittenRead(e: IrExpr, writes: Writes): boolean {
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null) return false;
|
||||
const keys = rw.readKeys(e, (call) => this.borrowedReturnKeys(call.callee));
|
||||
// A checked extraction reads nothing besides the union it narrows.
|
||||
const keys = rw.readKeys(e, (call) =>
|
||||
borrowsCheckedNarrow(this, call) ? [] : this.borrowedReturnKeys(call.callee),
|
||||
);
|
||||
return keys !== null && !rw.conflicts(keys, writes);
|
||||
}
|
||||
|
||||
@@ -5964,7 +6076,21 @@ export class LlEmitter {
|
||||
}
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
case "recordKeyGet":
|
||||
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.obj);
|
||||
case "mapIntrinsic": {
|
||||
// A Map binding's own pointer (maps are not plainReference types).
|
||||
if (!this.borrowsWithoutOwning(e) || e.receiver.kind !== "varRef") return false;
|
||||
const binding = this.binding(e.receiver.localId);
|
||||
return (
|
||||
binding.kind === "global" ||
|
||||
(binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
!binding.local.boxed &&
|
||||
!binding.local.tdz &&
|
||||
!this.localUnionStorage.has(e.receiver.localId))
|
||||
);
|
||||
}
|
||||
case "unionNarrow":
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
@@ -6044,6 +6170,8 @@ export class LlEmitter {
|
||||
if (this.B.isTerminated()) return;
|
||||
if (cursor) cursor.index = i;
|
||||
this.stmtCursor = cursor;
|
||||
const reserve = reservedArrayLength(stmts, i);
|
||||
if (reserve !== null) this.reservedArrayLengths.set(reserve.literal, reserve.hint);
|
||||
const deferred = this.deferredPromiseAllLocal(stmts, i);
|
||||
if (deferred !== null) {
|
||||
this.deferredPromiseAllEntries.set((stmts[i] as { localId: string }).localId, deferred);
|
||||
@@ -6686,6 +6814,12 @@ export class LlEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Arrays of this element type can hold SCR_ARR_FOREIGN elements. */
|
||||
mayHoldForeign(elem: IrType): boolean {
|
||||
if (this.foreignElementKeys.size === 0) return false;
|
||||
return this.foreignElementKeys.has("*") || this.foreignElementKeys.has(typeKey(elem));
|
||||
}
|
||||
|
||||
/** Lifetime regions in modules with lifetimeRegions (llvm/regions.ts).
|
||||
* Created with the class graph; null elsewhere. */
|
||||
regions: LifetimeRegions | null = null;
|
||||
@@ -7214,6 +7348,11 @@ export class LlEmitter {
|
||||
return this.nullableFieldGet(e) === null;
|
||||
case "recordGet":
|
||||
return true;
|
||||
// A keyed read whose hits are all the result's own pointer.
|
||||
case "recordKeyGet":
|
||||
return pointerRecordKeyGet(this, e);
|
||||
case "mapIntrinsic":
|
||||
return pointerMapGet(this, e);
|
||||
// emitReadReceiver lends a required element of a borrowed array when
|
||||
// computing the index removes no edge.
|
||||
case "arrayGet":
|
||||
@@ -7359,8 +7498,20 @@ export class LlEmitter {
|
||||
return wrapEmitterListener(this, target, adapterFn);
|
||||
}
|
||||
|
||||
unwrapNullableClosure(u: string, funcTag: number): string {
|
||||
return unwrapNullableClosure(this, u, funcTag);
|
||||
/** ScrClosure.function_kind for a closure over `fn`: generator 1, async
|
||||
* 2, own prototype 4, and SCR_FN_RECEIVER_FREE (16) when its body never
|
||||
* observes the ambient receiver, so indirect callers skip the bracket. */
|
||||
closureFunctionKind(fn: IrFunction): number {
|
||||
return (
|
||||
(fn.generator ? 1 : 0) +
|
||||
(fn.async ? 2 : 0) +
|
||||
(fn.ownsPrototype ? 4 : 0) +
|
||||
(this.receiverReaders.ignores(fn) ? CLOSURE_RECEIVER_FREE : 0)
|
||||
);
|
||||
}
|
||||
|
||||
unwrapNullableClosure(u: string, unionId: string, funcTag: number): string {
|
||||
return unwrapNullableClosure(this, u, unionId, funcTag);
|
||||
}
|
||||
|
||||
closeBindThunkFor(cbUnion: IrType, retServer: boolean): string {
|
||||
|
||||
@@ -372,6 +372,21 @@ export function emitSerializationExpr(
|
||||
// the dummy harmlessly.
|
||||
// `JSON.parse(text) as T` fuses into one schema-directed parse that
|
||||
// falls back to exactly this route whenever the fast path declines.
|
||||
// A package-declared target: the cold path names the declaration in
|
||||
// the pending TypeError (scr_type_origin.c).
|
||||
// Library archives and reactors link a fixed runtime set without the
|
||||
// note unit: their checks keep the plain message.
|
||||
const origin =
|
||||
e.origin === undefined || host.mod.lib !== undefined
|
||||
? undefined
|
||||
: host.mod.declOrigins?.[e.origin];
|
||||
const note =
|
||||
origin === undefined
|
||||
? undefined
|
||||
: (): void => {
|
||||
host.declare("declare void @scr_type_origin_note(ptr)");
|
||||
B.line(`call void @scr_type_origin_note(ptr ${host.cstr(origin)})`);
|
||||
};
|
||||
const parsed = e.value;
|
||||
if (!e.preserveRefs && parsed.kind === "libCall" && parsed.fn === "json.parse") {
|
||||
const fused = host.dyn.jsonParseHelper(e.type);
|
||||
@@ -380,7 +395,7 @@ export function emitSerializationExpr(
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${fused}(ptr ${text.name})`);
|
||||
const fusedOut = host.own({ name: r, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
host.emitPendingCheck(undefined, false, undefined, note);
|
||||
return fusedOut;
|
||||
}
|
||||
}
|
||||
@@ -402,7 +417,7 @@ export function emitSerializationExpr(
|
||||
B.line(`call void @scr_dyn_inferred_site(ptr ${site})`);
|
||||
}
|
||||
if (isDynTypedRefType(e.type)) {
|
||||
const checked = emitClassCheck(host, e.type, dynV.name, helper);
|
||||
const checked = emitClassCheck(host, e.type, dynV.name, helper, note);
|
||||
if (site !== null) B.line(`call void @scr_dyn_inferred_site(ptr null)`);
|
||||
return checked;
|
||||
}
|
||||
@@ -413,7 +428,7 @@ export function emitSerializationExpr(
|
||||
);
|
||||
if (site !== null) B.line(`call void @scr_dyn_inferred_site(ptr null)`);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
host.emitPendingCheck(undefined, false, undefined, note);
|
||||
return out;
|
||||
}
|
||||
default: {
|
||||
@@ -432,6 +447,7 @@ function emitClassCheck(
|
||||
type: IrType & { kind: "object" },
|
||||
value: string,
|
||||
helper: string,
|
||||
note?: () => void,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const slot = B.slot();
|
||||
@@ -472,7 +488,7 @@ function emitClassCheck(
|
||||
B.line(`${result} = call ptr @${helper}(ptr ${value}, ptr null)`);
|
||||
B.line(`store ptr ${result}, ptr ${slot}`);
|
||||
// A throwing check returns null, so the unwind has nothing to release.
|
||||
host.emitPendingCheck();
|
||||
host.emitPendingCheck(undefined, false, undefined, note);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import type { BytesHeader } from "./bytes-headers.js";
|
||||
import { BYTES_ELEMENT_SIZE, F64, type IrBytesElem, type IrExpr } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
|
||||
import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js";
|
||||
@@ -8,7 +9,7 @@ import { alwaysExactInteger, exactInteger } from "./integer-values.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { emitByteNumber } from "./byte-numbers.js";
|
||||
import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias, bytesHeaderValueAlias } from "./bytes-alias.js";
|
||||
|
||||
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
|
||||
u8: { kind: 0, le: false },
|
||||
@@ -160,16 +161,43 @@ export function emitBytesIndex(
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** The element count of a typed array, in the target's size type. */
|
||||
export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string {
|
||||
/** Records that `value` is the view `receiver` reads, so the header reads of
|
||||
* an access through it use the values a loop entry read (bytes-headers.ts). */
|
||||
export function noteBytesReceiver(host: LlvmEmitterContext, receiver: IrExpr, value: string): void {
|
||||
const scope = host.bytesHeaders;
|
||||
if (scope === null || receiver.kind !== "varRef" || scope.fn !== host.currentFunctionName) return;
|
||||
const header = scope.locals.get(receiver.localId);
|
||||
if (header !== undefined) scope.values.set(value, header);
|
||||
}
|
||||
|
||||
/** The header fields a loop entry read for the view `receiver`, if any. */
|
||||
function cachedHeader(host: LlvmEmitterContext, receiver: string): BytesHeader | undefined {
|
||||
const scope = host.bytesHeaders;
|
||||
if (scope === null || scope.fn !== host.currentFunctionName) return undefined;
|
||||
return scope.values.get(receiver);
|
||||
}
|
||||
|
||||
/** The element count of a typed array, in the target's size type. `value`:
|
||||
* the program reads the count itself (bytesHeaderValueAlias), rather than
|
||||
* only bounding an access. */
|
||||
export function emitBytesHeaderLength(
|
||||
host: LlvmEmitterContext,
|
||||
receiver: string,
|
||||
value = false,
|
||||
): string {
|
||||
const cached = cachedHeader(host, receiver);
|
||||
if (cached !== undefined) return cached.length;
|
||||
const p = host.B.tmp();
|
||||
const len = host.B.tmp();
|
||||
const alias = value ? bytesHeaderValueAlias(host) : bytesHeaderAlias(host);
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${alias}`);
|
||||
return len;
|
||||
}
|
||||
|
||||
export function emitBytesData(host: LlvmEmitterContext, receiver: string): string {
|
||||
const cached = cachedHeader(host, receiver);
|
||||
if (cached !== undefined) return cached.data;
|
||||
const p = host.B.tmp();
|
||||
const data = host.B.tmp();
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 3`);
|
||||
@@ -184,7 +212,7 @@ export function emitBytesLength(
|
||||
bytes: boolean,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
const len = emitBytesHeaderLength(host, receiver, true);
|
||||
const size = BYTES_ELEMENT_SIZE[elem];
|
||||
const count = bytes && size !== 1 ? B.tmp() : len;
|
||||
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`);
|
||||
@@ -196,6 +224,8 @@ export function emitBytesLength(
|
||||
/** A view's shared backing (null for a buffer this thread owns). It is
|
||||
* set at construction, before script code can see the view. */
|
||||
export function emitBytesSharedStorage(host: LlvmEmitterContext, receiver: string): string {
|
||||
const cached = cachedHeader(host, receiver)?.shared;
|
||||
if (cached !== undefined && cached !== null) return cached;
|
||||
const B = host.B;
|
||||
const slot = B.tmp(),
|
||||
storage = B.tmp();
|
||||
@@ -296,7 +326,7 @@ function emitScopedBytesGet(
|
||||
const signed = elem === "i8" || elem === "i16" || elem === "i32";
|
||||
B.line(`${number} = ${signed ? "sitofp" : "uitofp"} i32 ${u32} to double`);
|
||||
B.line(`${out} = select i1 ${valid}, double ${number}, double ${f64Lit(NaN)}`);
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
return { name: out, type: F64, uint32: u32, ordered: valid };
|
||||
}
|
||||
|
||||
function emitBytesLoad(
|
||||
@@ -310,7 +340,7 @@ function emitBytesLoad(
|
||||
owned: boolean,
|
||||
): LlValue {
|
||||
// `owned`: an enclosing version guard proved the view thread-owned.
|
||||
if (host.mod.workers && !owned) {
|
||||
if (host.mod.workers && !owned && !plainSharedElement(host, elem, expr)) {
|
||||
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
|
||||
return emitSharedOrLocalLoad(host, elem, receiver, idx);
|
||||
}
|
||||
@@ -321,6 +351,31 @@ function emitBytesLoad(
|
||||
return emitBytesLoadAt(host, elem, data, idx);
|
||||
}
|
||||
|
||||
/** Worker programs: whether an element access at `index` of a view that
|
||||
* may be shared is a plain load or store, like a thread-owned view's. It is
|
||||
* when the index is the counter of the strip-mined counted loop whose body
|
||||
* holds the access, outside any loop nested in that body (its condition and
|
||||
* update included: a nested loop that waits on the element reads it on
|
||||
* every iteration, so the read stays atomic): every iteration then reaches
|
||||
* a different element, so LLVM cannot merge an element's reads across
|
||||
* iterations or move them out of the loop, and the loop can be vectorized. A racing write from another thread can still be seen or
|
||||
* missed within one iteration, the documented gap of non-atomic shared
|
||||
* accesses. Float16 elements keep their runtime path. */
|
||||
function plainSharedElement(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
index: IrExpr | undefined,
|
||||
): boolean {
|
||||
const loop = host.plainSharedLoop;
|
||||
if (loop === null || elem === "f16" || index?.kind !== "varRef") return false;
|
||||
// A nested loop raises loopDepth before its condition is emitted.
|
||||
if (index.localId !== loop.counter || host.loopDepth !== loop.depth) return false;
|
||||
let innermost: (typeof host.jumpTargets)[number] | undefined;
|
||||
for (let i = host.jumpTargets.length - 1; i >= 0 && innermost === undefined; i--)
|
||||
if (host.jumpTargets[i]!.kind === "loop") innermost = host.jumpTargets[i];
|
||||
return innermost === loop.target;
|
||||
}
|
||||
|
||||
/** Worker programs: the element at a checked index of a view that may be
|
||||
* shared. An integer element joins as its 32-bit value and converts to a
|
||||
* number after the join, as the local load does, so compares and
|
||||
@@ -659,7 +714,7 @@ function emitScopedBytesSet(
|
||||
ty = "i32";
|
||||
operand = stored!;
|
||||
}
|
||||
if (host.mod.workers && !owned) {
|
||||
if (host.mod.workers && !owned && !plainSharedElement(host, elem, indexExpr)) {
|
||||
// A shared view's element takes a relaxed atomic store of its width.
|
||||
const local = emitSharedBytesSplit(host, receiver);
|
||||
const p = B.tmp();
|
||||
@@ -781,6 +836,7 @@ export function emitBytesIntrinsic(
|
||||
const r = directElementAccess
|
||||
? host.emitStableReceiver(e.receiver, e.args)
|
||||
: emitBorrowedInput(host, e.receiver);
|
||||
if (directElementAccess) noteBytesReceiver(host, e.receiver, r.name);
|
||||
// Byte operations borrow every input; views and identity results return
|
||||
// independent owners, while writes retain no operand beyond the call.
|
||||
const args = e.args.map((a) => emitBorrowedInput(host, a));
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleGenResThunk, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import { FN_ATTRS, llFieldType, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
@@ -54,7 +54,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
const key = `race:${typeKey(from)}=>${typeKey(to)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_race_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_race_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "raceAdapterFor", [from, to]);
|
||||
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
|
||||
@@ -217,7 +217,7 @@ export function genResultThunkFor(
|
||||
const key = `gr:${typeKey(genT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = mangleGenResThunk(host.resolveThunks.size);
|
||||
sym = host.contentSymbol("sc_gres_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]);
|
||||
const shape = host.recordsById.get(recT.shapeId);
|
||||
@@ -474,7 +474,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
const key = `cx:${param.unionId}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cx_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cx_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childExitThunkFor", [param]);
|
||||
const def = host.unionsById.get(param.unionId);
|
||||
@@ -519,7 +519,7 @@ export function childExitSignalThunkFor(
|
||||
const key = `cx2:${codeParam.unionId}+${sigParam.unionId}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cx_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cx_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]);
|
||||
const codeDef = host.unionsById.get(codeParam.unionId);
|
||||
@@ -580,7 +580,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
const key = `exec:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_exec_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_exec_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "execFileThunkFor", [cbT]);
|
||||
const errorParam = cbT.params[0];
|
||||
@@ -652,7 +652,7 @@ export function ipcMessageThunkFor(
|
||||
const key = `ipcmsg:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ipc_msg_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ipc_msg_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]);
|
||||
const param = cbT.params[0];
|
||||
@@ -703,7 +703,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
const key = `ipcsend:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ipc_send_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ipc_send_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]);
|
||||
const param = cbT.params[0];
|
||||
@@ -764,7 +764,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
const key = `cd:${param.unionId}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cd_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cd_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childDataThunkFor", [param]);
|
||||
const def = host.unionsById.get(param.unionId);
|
||||
@@ -799,7 +799,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" |
|
||||
const key = `cdd:${chunk}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cdd_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cdd_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]);
|
||||
const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str";
|
||||
@@ -834,7 +834,7 @@ export function emitterFixedAdapter(
|
||||
const key = `ee:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return { fn: sym, shim };
|
||||
sym = `sc_ee_ad_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ee_ad_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
@@ -905,7 +905,7 @@ export function emitterOnceAdapter(
|
||||
: `eeOnce:${fire}:${typeKey(boxerT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return { fn: sym, shim };
|
||||
sym = `sc_ee_once_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ee_once_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
@@ -974,10 +974,20 @@ export function wrapEmitterListener(
|
||||
export function unwrapNullableClosure(
|
||||
host: LlvmEmitterContext,
|
||||
u: string,
|
||||
unionId: string,
|
||||
funcTag: number,
|
||||
): string {
|
||||
const B = host.B;
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
const nullable = host.nullableUnions.get(unionId);
|
||||
if (nullable !== null) {
|
||||
// The union is the closure pointer itself (NULL for the unit arm).
|
||||
if (nullable.repr !== "single" || nullable.nullTag >= 0 || nullable.refTag !== funcTag)
|
||||
throw new InternalCompilerError("llvm emitter bug: nullable callback union shape");
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr ${retainSym(host.shapeHost, { kind: "union", unionId })}(ptr ${u})`);
|
||||
return r;
|
||||
}
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${slot}`);
|
||||
@@ -1015,7 +1025,7 @@ export function closeBindThunkFor(
|
||||
const key = `ncb:${cbUnion.unionId}:${retServer ? "srv" : "void"}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ncb_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ncb_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]);
|
||||
const def = host.unionsById.get(cbUnion.unionId);
|
||||
@@ -1026,9 +1036,9 @@ export function closeBindThunkFor(
|
||||
"llvm emitter bug: bound-close callback union lacks its func arm",
|
||||
);
|
||||
const oneParam = funcArm.params.length === 1;
|
||||
const cbNullable = host.nullableUnions.get(cbUnion.unionId);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_net_server_close_direct(ptr, ptr)`);
|
||||
let trampoline: string | null = null;
|
||||
@@ -1072,13 +1082,11 @@ export function closeBindThunkFor(
|
||||
` %capsp = getelementptr inbounds %ScrClosure, ptr %self, i64 1`,
|
||||
` %bx = load ptr, ptr %capsp`,
|
||||
` %srv = call ptr @scr_box_get_ref(ptr %bx) ; +1`,
|
||||
` %tagp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 1`,
|
||||
` %tag = load i32, ptr %tagp`,
|
||||
...unionTagLines(cbNullable, "%cbu", "%tag"),
|
||||
` %isfn = icmp eq i32 %tag, ${funcTag}`,
|
||||
` br i1 %isfn, label %fn, label %go`,
|
||||
`fn:`,
|
||||
` %pp = getelementptr inbounds %ScrUnion, ptr %cbu, i64 0, i32 5`,
|
||||
` %pv = load ptr, ptr %pp`,
|
||||
...unionPeekLines(cbNullable, "%cbu", "%pv"),
|
||||
];
|
||||
if (oneParam) {
|
||||
d.push(
|
||||
@@ -1097,7 +1105,7 @@ export function closeBindThunkFor(
|
||||
` br label %go`,
|
||||
`go:`,
|
||||
` %regv = load ptr, ptr %regslot`,
|
||||
` call void @scr_union_release(ptr %cbu) ; the callee owns its +1 param`,
|
||||
` call void ${releaseSym(host.shapeHost, cbUnion)}(ptr %cbu) ; the callee owns its +1 param`,
|
||||
` call void @scr_net_server_close_direct(ptr %srv, ptr %regv) ; reg moves`,
|
||||
);
|
||||
if (retServer) {
|
||||
@@ -1121,7 +1129,7 @@ export function closeOverrideWrapFor(
|
||||
const key = `ncw:${cbUnion.unionId}:${retServer ? "srv" : "void"}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ncw_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ncw_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]);
|
||||
const def = host.unionsById.get(cbUnion.unionId);
|
||||
@@ -1169,7 +1177,7 @@ export function settledStoreFor(
|
||||
const key = `settled:${typeKey(from)}=>${typeKey(to)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_settled_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_settled_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "settledStoreFor", [from, to]);
|
||||
const def = host.unionsById.get(to.unionId);
|
||||
|
||||
@@ -42,7 +42,13 @@ import {
|
||||
} from "./classes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./common.js";
|
||||
import {
|
||||
CLOSURE_RECEIVER_FREE,
|
||||
f64Lit,
|
||||
ffiNativeTypeLl,
|
||||
ffiNativeParamLl,
|
||||
ffiNativeReturnLl,
|
||||
} from "./common.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitStackRecordKeyGet, scalarRecordKeyGet } from "./expr-records.js";
|
||||
import { emitCallArrayRead } from "./local-array-reads.js";
|
||||
@@ -761,8 +767,7 @@ export function emitCallExpr(
|
||||
`${c} = call ptr @scr_closure_new(ptr @${host.callTarget(e.fnName)}, ${host.sizeType} ${e.captures.length})`,
|
||||
);
|
||||
const out = host.own({ name: c, type: e.type });
|
||||
const functionKind =
|
||||
(target.generator ? 1 : 0) + (target.async ? 2 : 0) + (target.ownsPrototype ? 4 : 0);
|
||||
const functionKind = host.closureFunctionKind(target);
|
||||
if (functionKind) {
|
||||
const kindPtr = B.tmp();
|
||||
B.line(`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
|
||||
@@ -800,6 +805,25 @@ export function emitCallExpr(
|
||||
: 0;
|
||||
host.declare(`declare void @scr_closure_adopt_identity(ptr, ptr, i32)`);
|
||||
B.line(`call void @scr_closure_adopt_identity(ptr ${c}, ptr ${original}, i32 ${length})`);
|
||||
if (host.receiverReaders.forwardsOnly(target)) {
|
||||
// The adapter forwards the ambient receiver to its original,
|
||||
// so it ignores the receiver exactly when the original does.
|
||||
const fromPtr = B.tmp(),
|
||||
from = B.tmp(),
|
||||
bit = B.tmp(),
|
||||
toPtr = B.tmp(),
|
||||
to = B.tmp(),
|
||||
kind = B.tmp();
|
||||
B.line(
|
||||
`${fromPtr} = getelementptr inbounds %ScrClosure, ptr ${original}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${from} = load i32, ptr ${fromPtr}`);
|
||||
B.line(`${bit} = and i32 ${from}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${toPtr} = getelementptr inbounds %ScrClosure, ptr ${c}, i64 0, i32 4`);
|
||||
B.line(`${to} = load i32, ptr ${toPtr}`);
|
||||
B.line(`${kind} = or i32 ${to}, ${bit}`);
|
||||
B.line(`store i32 ${kind}, ptr ${toPtr}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
@@ -813,7 +837,18 @@ export function emitCallExpr(
|
||||
const ft = e.callee.type;
|
||||
if (e.args.length !== ft.params.length)
|
||||
throw new LlvmUnsupportedError("callValue:arity", e.loc);
|
||||
const callee = host.stackCallbacks.emit(host, e.callee) ?? emitBorrowedInput(host, e.callee);
|
||||
// A receiver that reads the ambient `this` binds what the callee
|
||||
// would see without a binding: the call forwards it unchanged and
|
||||
// needs no receiver bracket (signature adapters forward this way).
|
||||
const forwardsReceiver =
|
||||
e.receiver?.kind === "libCall" &&
|
||||
e.receiver.fn === "dyn.this" &&
|
||||
e.receiver.args.length === 0;
|
||||
const callee =
|
||||
host.stackCallbacks.emit(host, e.callee) ??
|
||||
(lendsCallee(host, e, forwardsReceiver)
|
||||
? host.emitReadReceiver(e.callee)
|
||||
: emitBorrowedInput(host, e.callee));
|
||||
// A class instance bound as the callee's `this` (`h.fn(x)`) is boxed
|
||||
// only if the callee reads it: the receiver window records the
|
||||
// instance (owned by this frame for the whole call) with its typed-ref
|
||||
@@ -825,7 +860,10 @@ export function emitCallExpr(
|
||||
!host.shapeHost.isErrorClass(e.receiver.value.type.className)
|
||||
? host.emitExpr(e.receiver.value)
|
||||
: null;
|
||||
const receiver = e.receiver === undefined || lazyReceiver ? null : host.emitExpr(e.receiver);
|
||||
const receiver =
|
||||
e.receiver === undefined || lazyReceiver || forwardsReceiver
|
||||
? null
|
||||
: host.emitExpr(e.receiver);
|
||||
const pushReceiver = (): void => {
|
||||
if (lazyReceiver) {
|
||||
const t = lazyReceiver.type;
|
||||
@@ -865,7 +903,7 @@ export function emitCallExpr(
|
||||
? borrowableInputs(host, e.args, false, host.referenceWrites?.summary(known!.name))
|
||||
: [];
|
||||
let dynamic = false;
|
||||
if (!direct && e.receiver === undefined) {
|
||||
if (!direct && (e.receiver === undefined || forwardsReceiver)) {
|
||||
const references = new Set<number>();
|
||||
ft.params.forEach((param, index) => {
|
||||
if (isRefCounted(param)) references.add(index);
|
||||
@@ -898,16 +936,51 @@ export function emitCallExpr(
|
||||
lent.has(i) ? { name: host.retainFrame(a.name, a.type), type: a.type } : a,
|
||||
)
|
||||
: args;
|
||||
const invoke = (target: string, needsReceiver: boolean, values = args): string => {
|
||||
// An entry through the closure's own pointer brackets the call only
|
||||
// when the closure may observe the receiver (SCR_FN_RECEIVER_FREE).
|
||||
const invoke = (
|
||||
target: string,
|
||||
bracket: boolean,
|
||||
values = args,
|
||||
indirect = false,
|
||||
): string => {
|
||||
const needsReceiver = bracket && !forwardsReceiver;
|
||||
let observes: string | null = null;
|
||||
if (needsReceiver) {
|
||||
host.declare("declare void @scr_dyn_this_pop()");
|
||||
pushReceiver();
|
||||
if (indirect) {
|
||||
const kindPtr = B.tmp(),
|
||||
kind = B.tmp(),
|
||||
free = B.tmp();
|
||||
observes = B.tmp();
|
||||
B.line(
|
||||
`${kindPtr} = getelementptr inbounds %ScrClosure, ptr ${callee.name}, i64 0, i32 4`,
|
||||
);
|
||||
B.line(`${kind} = load i32, ptr ${kindPtr}`);
|
||||
B.line(`${free} = and i32 ${kind}, ${CLOSURE_RECEIVER_FREE}`);
|
||||
B.line(`${observes} = icmp eq i32 ${free}, 0`);
|
||||
const push = B.newLabel("receiver.push"),
|
||||
pushed = B.newLabel("receiver.call");
|
||||
B.condBr(observes, push, pushed);
|
||||
B.startBlock(push);
|
||||
pushReceiver();
|
||||
B.br(pushed);
|
||||
B.startBlock(pushed);
|
||||
} else pushReceiver();
|
||||
}
|
||||
const result = e.type.kind === "void" ? "" : B.tmp();
|
||||
B.line(
|
||||
`${result ? `${result} = ` : ""}call ${host.llType(e.type)} ${target}(${argListOf(values)})`,
|
||||
);
|
||||
if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
if (observes !== null) {
|
||||
const pop = B.newLabel("receiver.pop"),
|
||||
popped = B.newLabel("receiver.done");
|
||||
B.condBr(observes, pop, popped);
|
||||
B.startBlock(pop);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
B.br(popped);
|
||||
B.startBlock(popped);
|
||||
} else if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
return result;
|
||||
};
|
||||
const lending =
|
||||
@@ -953,11 +1026,11 @@ export function emitCallExpr(
|
||||
B.line(`${has} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(has, borrowing, owning);
|
||||
B.startBlock(borrowing);
|
||||
const lentResult = invoke(entry, true);
|
||||
const lentResult = invoke(entry, true, args, true);
|
||||
const borrowingEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(owning);
|
||||
const ownedResult = invoke(fn, true, ownedArgs());
|
||||
const ownedResult = invoke(fn, true, ownedArgs(), true);
|
||||
const owningEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
@@ -966,7 +1039,7 @@ export function emitCallExpr(
|
||||
B.line(
|
||||
`${t} = phi ${host.llType(e.type)} [ ${lentResult}, %${borrowingEnd} ], [ ${ownedResult}, %${owningEnd} ]`,
|
||||
);
|
||||
} else if (!direct) t = invoke(fn, true, ownedArgs());
|
||||
} else if (!direct) t = invoke(fn, true, ownedArgs(), true);
|
||||
else {
|
||||
// A global initializer may not have run, or a callback may enter
|
||||
// through a boundary the census cannot prove. Validate the actual
|
||||
@@ -984,7 +1057,7 @@ export function emitCallExpr(
|
||||
const fastEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(fallback);
|
||||
const indirect = invoke(fn, true, ownedArgs());
|
||||
const indirect = invoke(fn, true, ownedArgs(), true);
|
||||
const fallbackEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
@@ -1358,6 +1431,22 @@ function ctorArguments(
|
||||
* walk consumes without owning it (LlEmitter.canBorrowReceiver proved every
|
||||
* reference argument borrowable and every argument edge-preserving). The
|
||||
* result stays reachable from the borrowed arguments until the consumer. */
|
||||
/** A closure value read from a location the call cannot write (a keyed
|
||||
* table entry, a field, a checked extraction of either) is entered without
|
||||
* a reference of its own: the location keeps the closure alive until the
|
||||
* call returns. Plain stable bindings already lend (emitBorrowedInput). */
|
||||
function lendsCallee(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"callValue">,
|
||||
forwardsReceiver: boolean,
|
||||
): boolean {
|
||||
const writes = host.referenceWrites;
|
||||
if (writes === null || host.canBorrowCallArgument(e.callee) || !host.canBorrowReceiver(e.callee))
|
||||
return false;
|
||||
const later = e.receiver === undefined || forwardsReceiver ? e.args : [e.receiver, ...e.args];
|
||||
return borrowableInputs(host, [e.callee, ...later], false, writes.ownWrites(e))[0] === true;
|
||||
}
|
||||
|
||||
export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call">): LlValue {
|
||||
const callee = host.fnByName.get(e.callee);
|
||||
if (!callee || !host.borrowedReturns.has(e.callee))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { dynDesc } from "../../ir/analysis.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { arraySearchHelperName, arraySearchKind } from "./array-search.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { F64, type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { mangleResolveThunk } from "../mangle.js";
|
||||
import {
|
||||
arrOps,
|
||||
elemAccess,
|
||||
@@ -49,11 +49,13 @@ import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
import { emitScratchStringSlice, emitStringSliceRead } from "./string-slices.js";
|
||||
import {
|
||||
STRING_CHAR_AT_DECL,
|
||||
STRING_CP_AT_BYTE_DECL,
|
||||
STRING_CHAR_AT_INT_MARKER,
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
emitAsciiChar,
|
||||
emitAsciiCharCode,
|
||||
emitStringByteLength,
|
||||
emitUnitCharCode,
|
||||
STRING_UTF16_LEN_DECL,
|
||||
} from "./string-reads.js";
|
||||
@@ -73,7 +75,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
|
||||
const key = typeKey(inner);
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (!sym) {
|
||||
sym = mangleResolveThunk(host.resolveThunks.size);
|
||||
sym = host.contentSymbol("sc_rslv_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "resolveThunkFor", [inner]);
|
||||
const v = vAdapters(host.shapeHost, inner);
|
||||
@@ -564,16 +566,30 @@ export function emitStrIntrinsic(
|
||||
return call("scr_str_is_well_formed", "zeroext i1 (ptr)", `ptr ${r.name}`, "i1", false);
|
||||
case "toWellFormed":
|
||||
return call("scr_str_to_well_formed", "ptr (ptr)", `ptr ${r.name}`, "ptr", true);
|
||||
case "cpAt":
|
||||
// The code point AT an index as a one-code-point string (+1) —
|
||||
// the string-for-of desugar's read.
|
||||
return call(
|
||||
"scr_str_cp_at",
|
||||
"ptr (ptr, double)",
|
||||
`ptr ${r.name}, double ${args[0]!.name}`,
|
||||
"ptr",
|
||||
true,
|
||||
);
|
||||
case "byteLength": {
|
||||
const t = B.tmp();
|
||||
// A byte count fits in a signed size, whose conversion is one instruction.
|
||||
B.line(`${t} = sitofp ${host.sizeType} ${emitStringByteLength(host, r.name)} to double`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "cpAtByte": {
|
||||
// sc_str_cp_at_byte (string-reads.ts) answers ASCII bytes from the
|
||||
// charAt table inline. The cursor is a character start, an exact
|
||||
// integer below the byte count.
|
||||
host.declare(STRING_CHAR_AT_DECL);
|
||||
host.declare(STRING_CP_AT_BYTE_DECL);
|
||||
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
|
||||
let wide: string;
|
||||
if (index) {
|
||||
wide = widenInteger(host, index);
|
||||
} else {
|
||||
wide = B.tmp();
|
||||
B.line(`${wide} = fptosi double ${args[0]!.name} to i64`);
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @sc_str_cp_at_byte(ptr ${r.name}, i64 ${wide})`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
default: {
|
||||
const _exhaustive: never = method;
|
||||
void _exhaustive;
|
||||
@@ -790,6 +806,9 @@ export function emitArrIntrinsic(
|
||||
case "shift": {
|
||||
host.emitPublishedArrayGuard(r.name, e.receiver.type, method === "pop" ? 2 : 6);
|
||||
const checkedValue = elem.kind === "dyn" && e.type.kind === "dyn";
|
||||
// A foreign element (SCR_ARR_FOREIGN) cannot be the typed result:
|
||||
// removing one throws the checked conversion's TypeError.
|
||||
const foreignElements = !checkedValue && host.mayHoldForeign(elem);
|
||||
if (!checkedValue && e.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: array removal result is not a union");
|
||||
const def = e.type.kind === "union" ? host.unionsById.get(e.type.unionId) : undefined;
|
||||
@@ -832,7 +851,25 @@ export function emitArrIntrinsic(
|
||||
);
|
||||
B.line(`${slowRaw} = load i64, ptr ${rawSlot}`);
|
||||
B.line(`${has} = icmp eq i8 ${state}, 1`);
|
||||
B.condBr(has, lp, la);
|
||||
const lf = foreignElements ? B.newLabel("remove.f") : la;
|
||||
B.condBr(has, lp, lf);
|
||||
if (foreignElements) {
|
||||
// Only the runtime call removes a foreign element; its value moves
|
||||
// out with the slot, and the throw releases it.
|
||||
B.startBlock(lf);
|
||||
const foreign = B.tmp();
|
||||
const throwLabel = B.newLabel("remove.foreign");
|
||||
B.line(`${foreign} = icmp eq i8 ${state}, 3`);
|
||||
B.condBr(foreign, throwLabel, la);
|
||||
B.startBlock(throwLabel);
|
||||
const payload = B.tmp();
|
||||
B.line(`${payload} = inttoptr i64 ${slowRaw} to ptr`);
|
||||
host.declare(`declare void @scr_dyn_foreign_throw(ptr, ptr)`);
|
||||
const want = host.cstr(dynDesc(e.type, host.recordsById, host.unionsById));
|
||||
B.line(`call void @scr_dyn_foreign_throw(ptr ${payload}, ptr ${want})`);
|
||||
host.emitAlwaysPendingUnwind();
|
||||
B.br(la);
|
||||
}
|
||||
B.startBlock(lp);
|
||||
let raw = slowRaw;
|
||||
const value = B.tmp();
|
||||
|
||||
@@ -21,6 +21,10 @@ export interface LlValue {
|
||||
* its absolute value is below `below` (a zero may be -0). Index consumers
|
||||
* use it in place of a round trip (integer-values.ts `indexInteger`). */
|
||||
wideInteger?: WideInteger;
|
||||
/** An i1 SSA value (or `true`) that is true exactly when this number is
|
||||
* not NaN: set by NaN-coded integer element reads and the wrapping sums
|
||||
* of them, so ToInt32 consumers need no floating compare. */
|
||||
ordered?: string;
|
||||
slot?: boolean;
|
||||
/** A sequence local held in a capture box; release the box itself. */
|
||||
boxed?: boolean;
|
||||
|
||||
@@ -2,11 +2,12 @@ import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, isUnitType, typeEquals, type IrExpr } from "../../ir/ir.js";
|
||||
import { isRefCounted, isUnitType, typeEquals, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { emitRecordKeyGetOr } from "./expr-records.js";
|
||||
import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js";
|
||||
import { emitUnionTag } from "./union-repr.js";
|
||||
|
||||
@@ -397,6 +398,13 @@ export function emitControlExpr(
|
||||
throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms");
|
||||
const fused = emitNullishOptionalChain(host, e);
|
||||
if (fused) return fused;
|
||||
if (
|
||||
e.left.kind === "recordKeyGet" &&
|
||||
e.left.missNaN !== true &&
|
||||
typeEquals(e.type, e.left.type) &&
|
||||
storesNoUnit(host, e.left)
|
||||
)
|
||||
return emitRecordKeyGetOr(host, e.left, e.right);
|
||||
if (
|
||||
!typeEquals(e.type, e.left.type) &&
|
||||
(e.left.kind === "varRef" ||
|
||||
@@ -491,10 +499,7 @@ export function emitOptionalChainReceiver(
|
||||
// needs no reference of its own while the body cannot remove an edge
|
||||
// (so nothing can overwrite the projection before the body finishes).
|
||||
const borrowReceiver =
|
||||
!multiple &&
|
||||
isRefCounted(narrowed) &&
|
||||
host.canBorrowReceiver(e.receiver) &&
|
||||
host.referenceEffects.preserves(e.body);
|
||||
!multiple && isRefCounted(narrowed) && borrowsChainReceiver(host, e.receiver, e.body);
|
||||
// An optional array read of a nullable-pointer union is the element
|
||||
// pointer itself, read inline and owned by the statement frame until
|
||||
// the body finishes; the bind slot borrows it like an emitExpr value.
|
||||
@@ -554,6 +559,16 @@ export function emitOptionalChainReceiver(
|
||||
};
|
||||
}
|
||||
|
||||
/** An optional chain's receiver can lend its payload to the bind slot when
|
||||
* the body leaves the receiver's locations alone: it removes no edge at
|
||||
* all, or it writes none of the locations the receiver reads. */
|
||||
function borrowsChainReceiver(host: LlvmEmitterContext, receiver: IrExpr, body: IrExpr): boolean {
|
||||
if (!host.canBorrowReceiver(receiver)) return false;
|
||||
if (host.referenceEffects.preserves(body)) return true;
|
||||
const writes = host.referenceWrites;
|
||||
return writes !== null && host.unwrittenRead(receiver, writes.expr(body));
|
||||
}
|
||||
|
||||
/** `a?.b ?? d` narrowed to the chain body's present arm. The body wraps a
|
||||
* value of the result type in a non-unit arm, so the default runs exactly
|
||||
* when the receiver is nullish and the intermediate result box is never
|
||||
@@ -580,16 +595,21 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
|
||||
if (unitTags.length === 0 || present.length !== 1) return null;
|
||||
const B = host.B;
|
||||
const narrowed = receiverDef.arms[present[0]!]!;
|
||||
const r = host.canStackReceiver(chain.receiver)
|
||||
? host.emitReadReceiver(chain.receiver)
|
||||
: host.emitExpr(chain.receiver);
|
||||
// A receiver read from a location the body cannot write lends its
|
||||
// payload to the bind slot for the whole body (`table[k]?.(…) ?? d`).
|
||||
const borrowed =
|
||||
isRefCounted(narrowed) && borrowsChainReceiver(host, chain.receiver, chain.body.value);
|
||||
const r =
|
||||
borrowed || host.canStackReceiver(chain.receiver)
|
||||
? host.emitReadReceiver(chain.receiver)
|
||||
: host.emitExpr(chain.receiver);
|
||||
const bindTy = host.llType(narrowed);
|
||||
const bind = B.slot();
|
||||
B.entryAllocas.push(`${bind} = alloca ${bindTy}`);
|
||||
B.line(
|
||||
`store ${bindTy} ${bindTy === "ptr" ? "null" : bindTy === "double" ? f64Lit(0) : "false"}, ptr ${bind}`,
|
||||
);
|
||||
host.ownSlot(bind, narrowed);
|
||||
if (!borrowed) host.ownSlot(bind, narrowed);
|
||||
const isUnit = host.tagInSet(r.name, receiverDef.id, unitTags);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
@@ -602,7 +622,10 @@ function emitNullishOptionalChain(host: LlvmEmitterContext, e: ExprOf<"nullish">
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lb);
|
||||
B.line(`store ${bindTy} ${host.unionExtract(r.name, receiverDef.id, narrowed)}, ptr ${bind}`);
|
||||
const payload = borrowed
|
||||
? host.unionPeek(r.name, receiverDef.id)
|
||||
: host.unionExtract(r.name, receiverDef.id, narrowed);
|
||||
B.line(`store ${bindTy} ${payload}, ptr ${bind}`);
|
||||
host.chainSlots.set(chain.id, { name: bind, type: narrowed, slot: true });
|
||||
host.emitBranchInto(slot, chain.body.value);
|
||||
host.chainSlots.delete(chain.id);
|
||||
@@ -700,3 +723,15 @@ function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string {
|
||||
B.line(`${t} = phi i1 [ true, %${yes} ], [ false, %${no} ]`);
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Every value a keyed read can hit is present (no stored undefined or
|
||||
* null), so only a missing key yields the read's unit arm. */
|
||||
function storesNoUnit(host: LlvmEmitterContext, e: ExprOf<"recordKeyGet">): boolean {
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
const holdsUnit = (t: IrType): boolean =>
|
||||
isUnitType(t) ||
|
||||
t.kind === "dyn" ||
|
||||
(t.kind === "union" && (host.unionsById.get(t.unionId)?.arms ?? []).some(isUnitType));
|
||||
if (shape.indexValue !== undefined && holdsUnit(shape.indexValue)) return false;
|
||||
return e.overflowOnly === true || !shape.fields.some((f) => holdsUnit(f.type));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitNullableIsTag, emitNullablePresent } from "./union-repr.js";
|
||||
import { isObjectArm } from "./nullable-unions.js";
|
||||
import { isPointerIdentityArm } from "./nullable-unions.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import {
|
||||
DYN,
|
||||
@@ -31,7 +31,8 @@ import { elemAccess, vAdapters } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { emitJsIcLiteralMember, emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { jsIcLiteralKey } from "../../ir/js-ic.js";
|
||||
import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js";
|
||||
|
||||
export function emitDynamicExpr(
|
||||
@@ -316,9 +317,14 @@ export function emitDynamicExpr(
|
||||
const obj = B.tmp();
|
||||
B.line(`${obj} = call ptr @scr_dyn_new_obj()`);
|
||||
const out = host.own({ name: obj, type: e.type });
|
||||
const cached = jsIcMethods(host);
|
||||
for (const f of e.fields ?? []) {
|
||||
const k = emitBorrowedInput(host, f.key);
|
||||
const v = emitBorrowedInput(host, f.value);
|
||||
if (cached && jsIcLiteralKey(f.key) !== null) {
|
||||
emitJsIcLiteralMember(host, obj, k.name, v.name);
|
||||
continue;
|
||||
}
|
||||
B.line(
|
||||
`call void @${f.key.type.kind === "dyn" ? "scr_dyn_key_set_computed" : "scr_dyn_key_set"}(ptr ${obj}, ptr ${k.name}, ptr ${v.name})`,
|
||||
);
|
||||
@@ -902,13 +908,13 @@ export function emitDynamicExpr(
|
||||
if (!def)
|
||||
throw new InternalCompilerError(`llvm emitter bug: equality of unknown union ${e.unionId}`);
|
||||
const nullable = host.nullableUnions.get(def.id);
|
||||
if (nullable && isObjectArm(nullable)) {
|
||||
if (nullable && isPointerIdentityArm(nullable)) {
|
||||
// Object identity, and NULL === NULL for the unit arm: one compare.
|
||||
const eq = B.tmp();
|
||||
B.line(`${eq} = icmp ${e.negated ? "ne" : "eq"} ptr ${l.name}, ${r.name}`);
|
||||
return { name: eq, type: e.type };
|
||||
}
|
||||
if (nullable) {
|
||||
if (nullable && nullable.repr === "single" && nullable.arm.kind === "string") {
|
||||
// String arm: identical pointers are equal (the same string or the
|
||||
// same unit encoding); otherwise equal only when both hold strings
|
||||
// with the same bytes.
|
||||
|
||||
@@ -278,6 +278,18 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
returning("float_read_add", bin("|", bin("+", length, read("floats")), zero)),
|
||||
returning("product", bin("|", bin("*", length, length), zero)),
|
||||
returning("number_add", bin("+", length, read("bytes"))),
|
||||
returning(
|
||||
"read_products",
|
||||
bin(
|
||||
">>>",
|
||||
bin(
|
||||
"+",
|
||||
bin("*", read("bytes"), { kind: "numLit", value: 77, type: F64, loc }),
|
||||
bin("*", read("bytes"), { kind: "numLit", value: 150, type: F64, loc }),
|
||||
),
|
||||
zero,
|
||||
),
|
||||
),
|
||||
];
|
||||
const llvm = emitLlvmModule(mod);
|
||||
const body = (name: string): string =>
|
||||
@@ -292,8 +304,10 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
expect(body(name), name).not.toContain("uint32.coerce");
|
||||
}
|
||||
// A NaN-coded read converts to 0 through the sum: the result selects 0
|
||||
// unless every NaN-coded operand is ordered.
|
||||
expect(body("read_add")).toMatch(/ = fcmp ord double .*, 0\.0$/m);
|
||||
// unless every NaN-coded operand is ordered. A read's own validity bit
|
||||
// tells, so the number is not compared.
|
||||
expect(body("read_add")).toMatch(/ = phi i1 \[ true, /);
|
||||
expect(body("read_add")).not.toMatch(/fcmp ord/);
|
||||
expect(body("read_add")).toMatch(/ = add i32 /);
|
||||
expect(body("read_add")).toMatch(/ = select i1 .*, i32 .*, i32 0$/m);
|
||||
expect(body("read_chain")).toMatch(/ = and i1 /);
|
||||
@@ -309,6 +323,12 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
expect(body(name), name).not.toMatch(/ = (add|mul) i32 /);
|
||||
}
|
||||
expect(body("number_add")).not.toMatch(/ = add i32 |fcmp ord/);
|
||||
// Products of NaN-coded reads and constants are exact integers or NaN, so
|
||||
// their sum adds in 32 bits too, selected by the reads' validity bits.
|
||||
expect(body("read_products")).toMatch(/ = mul i32 /);
|
||||
expect(body("read_products")).toMatch(/ = add i32 /);
|
||||
expect(body("read_products")).not.toContain("uint32.coerce");
|
||||
expect(body("read_products")).not.toMatch(/fcmp ord/);
|
||||
});
|
||||
|
||||
test.each([32, 64] as const)(
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitWrappingSum, integerOrNaNRange, markWrappingOperands } from "./wrapping-sums.js";
|
||||
import { emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js";
|
||||
import { emitPowF64, emitPowerOfTwoQuotient, isPowerOfTwoCall } from "./lib-filesystem.js";
|
||||
|
||||
/** The signed integer predicates of the number compares. */
|
||||
const INTEGER_COMPARES: Record<string, string> = {
|
||||
@@ -305,7 +305,6 @@ export function emitOperatorExpr(
|
||||
"===": "oeq",
|
||||
"!==": "une",
|
||||
};
|
||||
const libm: Record<string, string> = { "**": "pow" };
|
||||
const bit: Record<string, "and" | "or" | "xor" | "shl" | "ashr" | "lshr"> = {
|
||||
"&": "and",
|
||||
"|": "or",
|
||||
@@ -367,8 +366,8 @@ export function emitOperatorExpr(
|
||||
`${result} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
|
||||
);
|
||||
const number = integerNumber(host, result, range, e.type);
|
||||
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
|
||||
return uint32 === undefined ? number : { ...number, uint32 };
|
||||
const wrapped = emitWrappingSum(host, e, l, r, li, ri);
|
||||
return wrapped === undefined ? number : { ...number, ...wrapped };
|
||||
}
|
||||
if (e.op === "%" && li.range.min >= 0 && ri.range.min > 0) {
|
||||
const range = { min: 0, max: Math.min(li.range.max, ri.range.max - 1) };
|
||||
@@ -411,12 +410,12 @@ export function emitOperatorExpr(
|
||||
return { name: emitPowerOfTwoQuotient(host, l.name, r.name), type: e.type };
|
||||
if (arith[e.op] !== undefined) {
|
||||
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
|
||||
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
|
||||
const wrapped = emitWrappingSum(host, e, l, r, li, ri);
|
||||
// The exact integer beside the double, for index consumers.
|
||||
const wideInteger = wideArithmetic(host, e.op, l, li, r, ri);
|
||||
if (uint32 !== undefined || wideInteger) {
|
||||
if (wrapped !== undefined || wideInteger) {
|
||||
const value: LlValue = { name: t, type: e.type };
|
||||
if (uint32 !== undefined) value.uint32 = uint32;
|
||||
if (wrapped !== undefined) Object.assign(value, wrapped);
|
||||
if (wideInteger) value.wideInteger = wideInteger;
|
||||
return value;
|
||||
}
|
||||
@@ -471,32 +470,8 @@ export function emitOperatorExpr(
|
||||
B.line(`${t} = ${e.op === ">>>" ? "uitofp" : "sitofp"} i32 ${result} to double`);
|
||||
return { name: t, type: e.type, uint32: result };
|
||||
} else {
|
||||
const fn = libm[e.op];
|
||||
if (fn === undefined) throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc);
|
||||
host.declare(`declare double @${fn}(double, double)`);
|
||||
// libm's pow answers 1 for pow(±1, ±Infinity) and pow(1, NaN), where
|
||||
// `**` answers NaN (and LLVM folds pow(1.0, x) to 1.0): an inline
|
||||
// guard selects NaN for a base of ±1 with a non-finite exponent and
|
||||
// keeps the direct pow call. A finite literal exponent, or a literal
|
||||
// base other than ±1, cannot meet those cases and needs no guard.
|
||||
if (fn === "pow" && !libmPowAgrees(e.left, e.right)) {
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
const power = B.tmp();
|
||||
B.line(`${power} = call double @pow(double ${l.name}, double ${r.name})`);
|
||||
const base = B.tmp();
|
||||
const unit = B.tmp();
|
||||
const magnitude = B.tmp();
|
||||
const finite = B.tmp();
|
||||
const reject = B.tmp();
|
||||
B.line(`${base} = call double @llvm.fabs.f64(double ${l.name})`);
|
||||
B.line(`${unit} = fcmp oeq double ${base}, 1.0`);
|
||||
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${r.name})`);
|
||||
B.line(`${finite} = fcmp olt double ${magnitude}, 0x7FF0000000000000`);
|
||||
B.line(`${reject} = select i1 ${finite}, i1 false, i1 ${unit}`);
|
||||
B.line(`${t} = select i1 ${reject}, double 0x7FF8000000000000, double ${power}`);
|
||||
} else {
|
||||
B.line(`${t} = call double @${fn}(double ${l.name}, double ${r.name})`);
|
||||
}
|
||||
if (e.op !== "**") throw new LlvmUnsupportedError(`bin:${e.op}`, e.loc);
|
||||
return { name: emitPowF64(host, l.name, r.name, e.right), type: e.type };
|
||||
}
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -649,7 +624,7 @@ export function regexLiteralTemplate(
|
||||
let re = host.regexInstances.get(key);
|
||||
if (!re) {
|
||||
re = {
|
||||
sym: `sc_re_${host.regexInstances.size}`,
|
||||
sym: host.contentSymbol("sc_re_", key),
|
||||
src: host.internLiteral(e.pattern),
|
||||
fl: host.internLiteral(e.flags),
|
||||
pattern: e.pattern,
|
||||
@@ -661,6 +636,29 @@ export function regexLiteralTemplate(
|
||||
return `@${re.sym}`;
|
||||
}
|
||||
|
||||
/** The build-time replacer of a global substitution of regex literal `e`
|
||||
* with the literal `replacement` (regex-matchers.ts), registered on first
|
||||
* use. */
|
||||
export function regexLiteralReplacer(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "regexLit" },
|
||||
replacement: string,
|
||||
): string {
|
||||
const key = `${e.flags}/${e.pattern}\0${replacement}`;
|
||||
let rr = host.regexReplacers.get(key);
|
||||
if (!rr) {
|
||||
rr = {
|
||||
sym: host.contentSymbol("sc_rr_", key),
|
||||
pattern: e.pattern,
|
||||
flags: e.flags,
|
||||
replacement,
|
||||
};
|
||||
host.regexReplacers.set(key, rr);
|
||||
}
|
||||
host.journal?.entry("regexReplacer", key, rr);
|
||||
return `@${rr.sym}`;
|
||||
}
|
||||
|
||||
/** A string equality or comparison between `s.charAt(i)` and a literal of
|
||||
* exactly one UTF-16 code unit: the charAt call, the unit, and whether the
|
||||
* charAt is the left operand. */
|
||||
@@ -946,6 +944,34 @@ export function emitStringExpr(
|
||||
}
|
||||
}
|
||||
|
||||
/** scr_arr_reserve_len reserves dense storage only up to this many slots
|
||||
* (SCR_ARR_DENSE_LIMIT, scr_array.c). */
|
||||
const RESERVE_LIMIT = 2 ** 20;
|
||||
|
||||
/** Elements the inline slots of a small array construction
|
||||
* (scr_arr_new_small, scr_arr_new_ops_small) hold at least
|
||||
* (SCR_ARR_SMALL_SLOTS, scr_array.c). */
|
||||
const SMALL_ARRAY_SLOTS = 3;
|
||||
|
||||
/** The capacity scr_arr_reserve_len would reserve for `length` on an empty
|
||||
* array: the length truncated when it is at least 1 and at most
|
||||
* RESERVE_LIMIT, else 0. */
|
||||
function reservedCapacity(host: LlvmEmitterContext, length: IrExpr): string {
|
||||
const B = host.B;
|
||||
const n = host.emitExpr(length);
|
||||
const atLeastOne = B.tmp(),
|
||||
withinLimit = B.tmp(),
|
||||
reserves = B.tmp(),
|
||||
slots = B.tmp(),
|
||||
capacity = B.tmp();
|
||||
B.line(`${atLeastOne} = fcmp oge double ${n.name}, ${f64Lit(1)}`);
|
||||
B.line(`${withinLimit} = fcmp ole double ${n.name}, ${f64Lit(RESERVE_LIMIT)}`);
|
||||
B.line(`${reserves} = and i1 ${atLeastOne}, ${withinLimit}`);
|
||||
B.line(`${slots} = select i1 ${reserves}, double ${n.name}, double ${f64Lit(0)}`);
|
||||
B.line(`${capacity} = fptoui double ${slots} to ${host.sizeType}`);
|
||||
return capacity;
|
||||
}
|
||||
|
||||
export function emitContainerExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -1017,7 +1043,34 @@ export function emitContainerExpr(
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
} else {
|
||||
const hint = host.reservedArrayLengths.get(e);
|
||||
if (hint !== undefined) {
|
||||
host.reservedArrayLengths.delete(e);
|
||||
host.consumedReserves.add(hint);
|
||||
const capacity = reservedCapacity(host, hint.length);
|
||||
// The small construction's inline slots hold at least
|
||||
// SMALL_ARRAY_SLOTS elements; larger capacities are sized.
|
||||
const fits = B.tmp(),
|
||||
smallArr = B.tmp(),
|
||||
sizedArr = B.tmp();
|
||||
const smallLabel = B.newLabel("arr.cap.small"),
|
||||
sizedLabel = B.newLabel("arr.cap.sized"),
|
||||
joined = B.newLabel("arr.cap.j");
|
||||
B.line(`${fits} = icmp ule ${host.sizeType} ${capacity}, ${SMALL_ARRAY_SLOTS}`);
|
||||
B.condBr(fits, smallLabel, sizedLabel);
|
||||
B.startBlock(smallLabel);
|
||||
B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(sizedLabel);
|
||||
B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(joined);
|
||||
B.line(
|
||||
`${arr} = phi ptr [ ${smallArr}, %${smallLabel} ], [ ${sizedArr}, %${sizedLabel} ]`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
}
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
let pending: LlValue[] = [];
|
||||
@@ -1395,19 +1448,3 @@ export function emitRecordLitFields(
|
||||
host.initField(ptr, type, inline, v);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether libm pow matches `**` for these operands: every case except a
|
||||
* base of ±1 with a NaN or infinite exponent agrees. */
|
||||
function libmPowAgrees(base: IrExpr, exponent: IrExpr): boolean {
|
||||
const power = numericLiteral(exponent);
|
||||
if (power !== null && Number.isFinite(power)) return true;
|
||||
const value = numericLiteral(base);
|
||||
return value !== null && !Number.isNaN(value) && Math.abs(value) !== 1;
|
||||
}
|
||||
|
||||
function numericLiteral(expr: IrExpr): number | null {
|
||||
if (expr.kind === "numLit") return expr.value;
|
||||
if (expr.kind === "unary" && expr.op === "-" && expr.operand.kind === "numLit")
|
||||
return -expr.operand.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
|
||||
import { f64Lit, llvmCommentText } from "./common.js";
|
||||
import { borrowableInputs, emitBorrowedInputs, emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { regexLiteralTemplate } from "./expr-primitives.js";
|
||||
import { regexLiteralReplacer, regexLiteralTemplate } from "./expr-primitives.js";
|
||||
import { canReplaceWith, regexLiteralCompiles } from "./regex-matchers.js";
|
||||
import { isScalarUnionDef, loadInlineUnion, presentBits } from "./inline-unions.js";
|
||||
import { preservesRegexInputs } from "./reference-effects.js";
|
||||
import { emitNumberMapSlot, emitStringMapEntry } from "./map-number-lookup.js";
|
||||
|
||||
/** Widen one tagged read. The source is borrowed unless the overflow map
|
||||
* supplied an owned value. Each selected reference payload is retained
|
||||
@@ -160,6 +162,39 @@ function directRegexOperandMethod(method: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** The build-time replacer of a substitution: a regex literal running on
|
||||
* its template, with a literal replacement the replacer can write as is.
|
||||
* Null when the substitution takes the runtime's loop. */
|
||||
function literalReplacer(
|
||||
host: LlvmEmitterContext,
|
||||
literal: IrExpr | null,
|
||||
template: string | null,
|
||||
replacement: IrExpr,
|
||||
): string | null {
|
||||
if (literal === null || template === null || literal.kind !== "regexLit") return null;
|
||||
if (replacement.kind !== "strLit") return null;
|
||||
if (!canReplaceWith(literal.pattern, literal.flags, replacement.value)) return null;
|
||||
if (!regexLiteralCompiles(literal.pattern, literal.flags)) return null;
|
||||
return regexLiteralReplacer(host, literal, replacement.value);
|
||||
}
|
||||
|
||||
/** scr_regex_replace_with: the runtime runs `replacer` on an ASCII subject
|
||||
* and its own loop on any other. */
|
||||
function emitReplaceWith(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr,
|
||||
r: LlValue,
|
||||
args: LlValue[],
|
||||
replacer: string,
|
||||
): LlValue {
|
||||
host.declare(`declare ptr @scr_regex_replace_with(ptr, ptr, ptr, ptr)`);
|
||||
const t = host.B.tmp();
|
||||
host.B.line(
|
||||
`${t} = call ptr @scr_regex_replace_with(ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${replacer})`,
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
export function emitRegexIntrinsic(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "regexIntrinsic" },
|
||||
@@ -234,11 +269,16 @@ export function emitRegexIntrinsic(
|
||||
"scr_regex_match_all_captures_into",
|
||||
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
|
||||
);
|
||||
case "replaceAll":
|
||||
case "replaceAll": {
|
||||
// A global literal (the only kind a replacer is built for) never takes
|
||||
// replaceAll's non-global TypeError.
|
||||
const replacer = literalReplacer(host, literal, template, e.args[1]!);
|
||||
if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer);
|
||||
return fallible(
|
||||
"scr_regex_replace_all",
|
||||
`ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`,
|
||||
);
|
||||
}
|
||||
case "split":
|
||||
host.declare(`declare ptr @scr_regex_split_limit(ptr, ptr, double)`);
|
||||
{
|
||||
@@ -323,6 +363,8 @@ export function emitRegexIntrinsic(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "replace": {
|
||||
const replacer = literalReplacer(host, literal, template, e.args[1]!);
|
||||
if (replacer !== null) return emitReplaceWith(host, e, r, args, replacer);
|
||||
host.declare(`declare ptr @scr_regex_replace(ptr, ptr, ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
@@ -338,13 +380,46 @@ export function emitRegexIntrinsic(
|
||||
}
|
||||
}
|
||||
|
||||
/** `r[k] ?? d` in its pass-through shape: a missing key evaluates `d`
|
||||
* where the read would store undefined, so a hit costs nothing extra. */
|
||||
export function emitRecordKeyGetOr(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
fallback: IrExpr,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const k = host.emitExpr(e.key);
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
const join = B.newLabel("rkg.j");
|
||||
keyedRecordReadInto(
|
||||
host,
|
||||
slot,
|
||||
join,
|
||||
obj.name,
|
||||
k.name,
|
||||
e.shapeId,
|
||||
e.type,
|
||||
e.overflowOnly === true,
|
||||
e.loc,
|
||||
false,
|
||||
() => host.emitBranchInto(slot, fallback),
|
||||
);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${ty}, ptr ${slot}`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
export function emitRecordKeyGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const obj = host.emitExpr(e.obj);
|
||||
const k = host.emitExpr(e.key);
|
||||
// The read runs no user code: both inputs lend for its duration.
|
||||
const [obj, k] = emitBorrowedInputs(host, [e.obj, e.key]) as [LlValue, LlValue];
|
||||
const ty = host.llType(e.type);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ${ty}`);
|
||||
@@ -366,6 +441,80 @@ export function emitRecordKeyGet(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
/** A keyed read whose every hit is the result's own pointer: the result
|
||||
* is a nullable-pointer union (nullable-unions.ts) with an undefined arm,
|
||||
* and each declared field and the overflow value hold exactly the result
|
||||
* type or its reference arm in a plain (non-inline) slot. A miss is the
|
||||
* immortal undefined encoding. Such a read can lend the stored pointer:
|
||||
* borrowedRecordKeyGet reads it without a reference. */
|
||||
export function pointerRecordKeyGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "recordKeyGet" || e.missNaN === true || e.type.kind !== "union") return false;
|
||||
const resultType = e.type;
|
||||
const nullable = host.nullableUnions.single(resultType.unionId);
|
||||
if (nullable === null || undefinedArmTag(resultType, host.unionsById) < 0) return false;
|
||||
const exact = (t: IrType): boolean => typeEquals(t, resultType) || typeEquals(t, nullable.arm);
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
if (e.overflowOnly !== true) {
|
||||
for (const f of shape.fields)
|
||||
if (host.recordFieldInline(e.shapeId, f.name) || !exact(f.type)) return false;
|
||||
}
|
||||
return shape.indexValue === undefined || typeEquals(shape.indexValue, nullable.arm);
|
||||
}
|
||||
|
||||
/** A pointerRecordKeyGet read without a reference: the declared field or
|
||||
* overflow entry the key names, or the undefined encoding for a miss. The
|
||||
* record (and through it the stored value) must stay unchanged and alive
|
||||
* until the consumer is done with the pointer (canBorrowReceiver). */
|
||||
export function emitBorrowedRecordKeyGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "recordKeyGet" },
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
if (e.type.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: borrowed keyed read of a non-union");
|
||||
const resultType = e.type;
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
const key = host.canBorrowReceiver(e.key) ? host.emitReadReceiver(e.key) : host.emitExpr(e.key);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const join = B.newLabel("rkb.j");
|
||||
const shape = host.recordShape(e.shapeId);
|
||||
for (const f of e.overflowOnly === true ? [] : shape.fields) {
|
||||
const hit = B.tmp();
|
||||
B.line(
|
||||
`${hit} = call zeroext i1 ${host.literalEquals(f.name)}(ptr ${key.name}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
const lh = B.newLabel("rkb.h");
|
||||
const ln = B.newLabel("rkb.n");
|
||||
B.condBr(hit, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const { ptr, type } = host.recordFieldPtr(obj.name, e.shapeId, f.name);
|
||||
B.line(`store ptr ${host.loadRecordField(ptr, type, false)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
if (shape.indexValue) {
|
||||
const ovf = host.recordOvfPtr(obj.name, e.shapeId);
|
||||
const entry = emitStringMapEntry(host, ovf, key.name);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
const lh = B.newLabel("rkb.h");
|
||||
const ln = B.newLabel("rkb.n");
|
||||
B.condBr(found, lh, ln);
|
||||
B.startBlock(lh);
|
||||
B.line(`store ptr ${emitNumberMapSlot(host, entry, "ptr")}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const undefTag = undefinedArmTag(resultType, host.unionsById);
|
||||
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
/** A keyed read whose result is a scalar union (`r[k]` on a
|
||||
* `Record<string, number>`, at `number | undefined`) and whose every hit
|
||||
* is a number or boolean arm of it, or an inline field of the same union.
|
||||
@@ -485,6 +634,7 @@ export function keyedRecordReadInto(
|
||||
overflowOnly: boolean,
|
||||
loc?: SrcLoc,
|
||||
missNaN = false,
|
||||
missInto: (() => void) | null = null,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const shape = host.recordShape(shapeId);
|
||||
@@ -660,35 +810,27 @@ export function keyedRecordReadInto(
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_map_get_str_ref(ptr, ptr)`);
|
||||
// The entry lookup filters absent keys inline; a hit retains the
|
||||
// stored value with its own type's retain (map-number-lookup.ts). A
|
||||
// nullable value's NULL is a stored unit arm: presence decides.
|
||||
const lh = B.newLabel("rkg.h");
|
||||
const ln = B.newLabel("rkg.n");
|
||||
let raw: string;
|
||||
if (iv.kind === "union" && host.nullableUnions.has(iv.unionId)) {
|
||||
// NULL is a stored unit arm of a nullable value: presence decides.
|
||||
host.declare(`declare zeroext i1 @scr_map_has_str(ptr, ptr)`);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = call zeroext i1 @scr_map_has_str(ptr ${ovf}, ptr ${keyName})`);
|
||||
B.condBr(has, lh, ln);
|
||||
B.startBlock(lh);
|
||||
raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
|
||||
} else {
|
||||
raw = B.tmp();
|
||||
const isnull = B.tmp();
|
||||
B.line(`${raw} = call ptr @scr_map_get_str_ref(ptr ${ovf}, ptr ${keyName})`);
|
||||
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
|
||||
B.condBr(isnull, ln, lh);
|
||||
B.startBlock(lh);
|
||||
}
|
||||
const entry = emitStringMapEntry(host, ovf, keyName);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(found, lh, ln);
|
||||
B.startBlock(lh);
|
||||
const raw = host.retainHeap(emitNumberMapSlot(host, entry, "ptr"), iv);
|
||||
B.line(`store ${ty} ${surface(iv, raw, true)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
}
|
||||
if (def !== null && resultType.kind === "union" && undefTag >= 0) {
|
||||
// The miss path: the result union's undefined arm.
|
||||
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
|
||||
// The miss path: the result union's undefined arm, or the caller's
|
||||
// own value for a miss (missInto stores it into the slot).
|
||||
if (missInto !== null) missInto();
|
||||
else B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,7 +184,11 @@ export function streamTypedRefCommitAdapter(
|
||||
}
|
||||
if (t.kind === "array") {
|
||||
const commit = `${snapshot}_commit`;
|
||||
const check = host.dyn.dynCheckHelper(t);
|
||||
// A value the element type cannot hold stays in the array as a foreign
|
||||
// element where the program can store one (LlEmitter.mayHoldForeign).
|
||||
const check = host.mayHoldForeign(t.elem)
|
||||
? host.dyn.dynViewArrayHelper(t)
|
||||
: host.dyn.dynCheckHelper(t);
|
||||
host.declare(`declare void @scr_arr_release(ptr)`);
|
||||
host.declare(`declare void @scr_arr_outline(ptr)`);
|
||||
host.declare(`declare void @scr_arr_adopt_view_properties(ptr, ptr, ptr)`);
|
||||
@@ -792,13 +796,16 @@ export function streamTypedRefMaterializeAdapter(
|
||||
t: IrType,
|
||||
): LlStreamTypedRefAdapter {
|
||||
const key = typeKey(t);
|
||||
const existing = host.liveDynRefAdapters.get(key);
|
||||
// A `(T | undefined)[]` stored as T elements shares T[]'s capsule type,
|
||||
// but its adapters also store undefined elements.
|
||||
const adapterKey = t.kind === "array" && t.optionalElements ? `${key}?` : key;
|
||||
const existing = host.liveDynRefAdapters.get(adapterKey);
|
||||
if (existing) return existing;
|
||||
// The converter depends only on the static type. Intern the complete
|
||||
// graph across roots and union arms so recursive types emit once.
|
||||
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
|
||||
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
|
||||
host.liveDynRefAdapters.set(key, adapter);
|
||||
host.liveDynRefAdapters.set(adapterKey, adapter);
|
||||
host.journal?.allocated(
|
||||
snapshot.replace(/_materialize$/, ""),
|
||||
"streamTypedRefMaterializeAdapter",
|
||||
@@ -1232,6 +1239,21 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`call void @scr_dyn_arr_push_hole(ptr ${out})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(undefinedLabel);
|
||||
if (host.mayHoldForeign(elem)) {
|
||||
// A foreign element (SCR_ARR_FOREIGN) holds its own value.
|
||||
const foreign = B.tmp();
|
||||
const foreignLabel = B.newLabel("live.array.foreign");
|
||||
const presentUndefined = B.newLabel("live.array.present.undefined");
|
||||
B.line(`${foreign} = fcmp oeq double ${state}, 3.0`);
|
||||
B.condBr(foreign, foreignLabel, presentUndefined);
|
||||
B.startBlock(foreignLabel);
|
||||
host.declare(`declare ptr @scr_arr_get_foreign(ptr, double)`);
|
||||
const payload = B.tmp();
|
||||
B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %p, double ${index}) ; +1`);
|
||||
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${payload})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(presentUndefined);
|
||||
}
|
||||
const undefinedValue = B.tmp();
|
||||
const retained = B.tmp();
|
||||
B.line(`${undefinedValue} = call ptr @scr_dyn_undefined()`);
|
||||
@@ -1312,13 +1334,34 @@ function liveArrayOps(
|
||||
G.terminate(`ret ptr ${boxed}`);
|
||||
|
||||
const S = new BlockBuilder();
|
||||
if (t.optionalElements) {
|
||||
// A `(T | undefined)[]` stores undefined as a present undefined.
|
||||
const undef = host.dyn.isUndefined(S, "%v");
|
||||
const storeUndefined = S.newLabel("live.array.undefined");
|
||||
const other = S.newLabel("live.array.value");
|
||||
S.condBr(undef, storeUndefined, other);
|
||||
S.startBlock(storeUndefined);
|
||||
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
||||
S.line(`call void @scr_arr_set_undefined(ptr %p, double %i)`);
|
||||
S.terminate(`ret i1 true`);
|
||||
S.startBlock(other);
|
||||
}
|
||||
const fits = S.tmp();
|
||||
S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(elem)}(ptr %v)`);
|
||||
const store = S.newLabel("live.array.store");
|
||||
const refuse = S.newLabel("live.array.refuse");
|
||||
S.condBr(fits, store, refuse);
|
||||
S.startBlock(refuse);
|
||||
S.terminate(`ret i1 false`);
|
||||
if (host.mayHoldForeign(elem)) {
|
||||
// The array keeps a value its element type cannot hold as a foreign
|
||||
// element, and undefined as a present undefined.
|
||||
host.declare(`declare zeroext i1 @scr_arr_set_foreign(ptr, double, ptr)`);
|
||||
const kept = S.tmp();
|
||||
S.line(`${kept} = call zeroext i1 @scr_arr_set_foreign(ptr %p, double %i, ptr %v)`);
|
||||
S.terminate(`ret i1 ${kept}`);
|
||||
} else {
|
||||
S.terminate(`ret i1 false`);
|
||||
}
|
||||
S.startBlock(store);
|
||||
const next = S.tmp();
|
||||
S.line(
|
||||
|
||||
@@ -13,7 +13,7 @@ export function streamDataAdapter(
|
||||
const key = `eed:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_ee_dad_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_ee_dad_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamDataAdapter", [cbT]);
|
||||
const p = cbT.params[0];
|
||||
@@ -221,7 +221,7 @@ export function streamDoneFnFor(
|
||||
const key = `sd:${kind}:${typeKey(doneT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_sd_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_sd_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]);
|
||||
const errT = doneT.params[0];
|
||||
@@ -365,7 +365,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
const key = `fsren:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_fsren_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_fsren_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]);
|
||||
const param = cbT.params[0]!;
|
||||
@@ -454,7 +454,7 @@ export function cryptoIntThunkFor(
|
||||
const key = `cryptoint:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_cryptoint_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_cryptoint_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]);
|
||||
const d: string[] = [
|
||||
@@ -516,7 +516,7 @@ export function zlibBytesThunkFor(
|
||||
const key = `${family}bytes:${arrayBuffer ? "arraybuffer:" : ""}${mode}${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_${family}bytes_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol(`sc_${family}bytes_`, key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]);
|
||||
const d: string[] = [
|
||||
@@ -644,7 +644,7 @@ export function streamCbThunkFor(
|
||||
const key = `scb:${kind}:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_scb_${host.resolveThunks.size}`;
|
||||
sym = host.contentSymbol("sc_scb_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]);
|
||||
const runtimeParams =
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/** Function layouts: the functions that runs of a program execute, in tiers
|
||||
* (scripts/refresh-cli-layout.mjs writes the native compiler's). The emitter
|
||||
* writes a listed function before the others, in a partition group of its
|
||||
* tier, so the code that runs together sits together in the executable. */
|
||||
|
||||
/** Layout key → tier (1 first). */
|
||||
export type FunctionLayout = ReadonlyMap<string, number>;
|
||||
|
||||
/** The layout key of an LLVM symbol: module numbers (`%m12.`, `%init.12`),
|
||||
* which change when modules are added, are left out. */
|
||||
export function functionLayoutKey(symbol: string): string {
|
||||
return symbol.replace(/_x25_m\d+_/g, "_x25_m_").replace(/_x25_init_\d+$/, "_x25_init_");
|
||||
}
|
||||
|
||||
/** Reads a layout's lines, each `<tier> <symbol>`. A function listed in two
|
||||
* tiers keeps the first. */
|
||||
export function parseFunctionLayout(text: string): FunctionLayout {
|
||||
const layout = new Map<string, number>();
|
||||
for (const line of text.split("\n")) {
|
||||
const space = line.indexOf(" ");
|
||||
if (space < 1) continue;
|
||||
const tier = Number(line.slice(0, space));
|
||||
const key = functionLayoutKey(line.slice(space + 1).trim());
|
||||
if (Number.isInteger(tier) && tier > 0 && !layout.has(key)) layout.set(key, tier);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
/** The lowest tier of the functions `text` defines, or undefined when the
|
||||
* layout lists none of them. */
|
||||
export function layoutTierOf(layout: FunctionLayout, text: string): number | undefined {
|
||||
let tier: number | undefined;
|
||||
for (let at = text.indexOf("define "); at >= 0; at = text.indexOf("\ndefine ", at + 1)) {
|
||||
const name = text.indexOf("@", at);
|
||||
const open = text.indexOf("(", name);
|
||||
if (name < 0 || open < 0) break;
|
||||
const listed = layout.get(functionLayoutKey(text.slice(name + 1, open)));
|
||||
if (listed !== undefined && (tier === undefined || listed < tier)) tier = listed;
|
||||
}
|
||||
return tier;
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
* keyed dense array element at an integral number key: inline;
|
||||
* otherwise scr_js_keyed_probe (string keys of ordinary objects)
|
||||
* method dynInvoke's method read: scr_js_load_probe
|
||||
* literal an object literal's constant-key member: scr_js_store_probe,
|
||||
* else scr_dyn_key_set
|
||||
*
|
||||
* A probe that cannot answer from plain data leaves everything untouched,
|
||||
* and the site calls its helper (or the generic runtime entry), which
|
||||
@@ -404,9 +406,13 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string)
|
||||
inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe);
|
||||
B.startBlock(probe);
|
||||
// The read's probe also answers keys the receiver's plain chain lacks.
|
||||
host.declare(`declare ptr @scr_js_get_probe(ptr, ptr, ptr)`);
|
||||
// Only `length` and `next` reads can be answered for a native array or
|
||||
// iterator, so only those sites take the probe that tests them.
|
||||
const probeFn =
|
||||
key === "length" || key === "next" ? "scr_js_get_probe_native" : "scr_js_get_probe";
|
||||
host.declare(`declare ptr @${probeFn}(ptr, ptr, ptr)`);
|
||||
const probed = B.tmp();
|
||||
B.line(`${probed} = call ptr @scr_js_get_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`);
|
||||
B.line(`${probed} = call ptr @${probeFn}(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${probed}, null`);
|
||||
const probeEnd = B.currentLabel();
|
||||
@@ -838,6 +844,27 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
/** One member of an object literal (ir/js-ic.ts jsIcLiteralKey) added to
|
||||
* the fresh object `obj` through the site's store cache, else with
|
||||
* scr_dyn_key_set. The key and value are borrowed. */
|
||||
export function emitJsIcLiteralMember(host: Host, obj: string, key: string, value: string): void {
|
||||
const B = host.B;
|
||||
const ic = host.jsIcSite();
|
||||
host.declare(`declare zeroext i1 @scr_js_store_probe(ptr, ptr, ptr, ptr)`);
|
||||
const done = B.tmp();
|
||||
B.line(
|
||||
`${done} = call zeroext i1 @scr_js_store_probe(ptr ${obj}, ptr ${key}, ptr ${value}, ptr ${ic})`,
|
||||
);
|
||||
const slow = B.newLabel("icl.slow");
|
||||
const join = B.newLabel("icl.j");
|
||||
B.condBr(done, join, slow);
|
||||
B.startBlock(slow);
|
||||
B.line(`call void @scr_dyn_key_set(ptr ${obj}, ptr ${key}, ptr ${value})`);
|
||||
host.emitPendingCheck();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
}
|
||||
|
||||
/** dynInvoke's method read on an ordinary object through the site's
|
||||
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
|
||||
* which scr_dyn_invoke_prepared_id answers with the full dispatch). */
|
||||
|
||||
@@ -17,7 +17,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str
|
||||
const key = `connect:${h2}:${typeKey(type)}`;
|
||||
const previous = host.resolveThunks.get(key);
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_connect_${host.resolveThunks.size}`;
|
||||
const symbol = host.contentSymbol("sc_connect_", key);
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "connectAdapter", [cb, h2]);
|
||||
const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user